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# Server
18#
19# See Bluetooth spec @ Vol 3, Part G
20#
21# -----------------------------------------------------------------------------
22
23# -----------------------------------------------------------------------------
24# Imports
25# -----------------------------------------------------------------------------
26from __future__ import annotations
27import asyncio
28import logging
29from collections import defaultdict
30import struct
31from typing import List, Tuple, Optional, TypeVar, Type, Dict, Iterable, TYPE_CHECKING
32from pyee import EventEmitter
33
34from bumble.colors import color
35from bumble.core import UUID
36from bumble.att import (
37    ATT_ATTRIBUTE_NOT_FOUND_ERROR,
38    ATT_ATTRIBUTE_NOT_LONG_ERROR,
39    ATT_CID,
40    ATT_DEFAULT_MTU,
41    ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
42    ATT_INVALID_HANDLE_ERROR,
43    ATT_INVALID_OFFSET_ERROR,
44    ATT_REQUEST_NOT_SUPPORTED_ERROR,
45    ATT_REQUESTS,
46    ATT_PDU,
47    ATT_UNLIKELY_ERROR_ERROR,
48    ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
49    ATT_Error,
50    ATT_Error_Response,
51    ATT_Exchange_MTU_Response,
52    ATT_Find_By_Type_Value_Response,
53    ATT_Find_Information_Response,
54    ATT_Handle_Value_Indication,
55    ATT_Handle_Value_Notification,
56    ATT_Read_Blob_Response,
57    ATT_Read_By_Group_Type_Response,
58    ATT_Read_By_Type_Response,
59    ATT_Read_Response,
60    ATT_Write_Response,
61    Attribute,
62)
63from bumble.gatt import (
64    GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
65    GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
66    GATT_MAX_ATTRIBUTE_VALUE_SIZE,
67    GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
68    GATT_REQUEST_TIMEOUT,
69    GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
70    Characteristic,
71    CharacteristicDeclaration,
72    CharacteristicValue,
73    IncludedServiceDeclaration,
74    Descriptor,
75    Service,
76)
77from bumble.utils import AsyncRunner
78
79if TYPE_CHECKING:
80    from bumble.device import Device, Connection
81
82# -----------------------------------------------------------------------------
83# Logging
84# -----------------------------------------------------------------------------
85logger = logging.getLogger(__name__)
86
87
88# -----------------------------------------------------------------------------
89# Constants
90# -----------------------------------------------------------------------------
91GATT_SERVER_DEFAULT_MAX_MTU = 517
92
93
94# -----------------------------------------------------------------------------
95# GATT Server
96# -----------------------------------------------------------------------------
97class Server(EventEmitter):
98    attributes: List[Attribute]
99    services: List[Service]
100    attributes_by_handle: Dict[int, Attribute]
101    subscribers: Dict[int, Dict[int, bytes]]
102    indication_semaphores: defaultdict[int, asyncio.Semaphore]
103    pending_confirmations: defaultdict[int, Optional[asyncio.futures.Future]]
104
105    def __init__(self, device: Device) -> None:
106        super().__init__()
107        self.device = device
108        self.services = []
109        self.attributes = []  # Attributes, ordered by increasing handle values
110        self.attributes_by_handle = {}  # Map for fast attribute access by handle
111        self.max_mtu = (
112            GATT_SERVER_DEFAULT_MAX_MTU  # The max MTU we're willing to negotiate
113        )
114        self.subscribers = (
115            {}
116        )  # Map of subscriber states by connection handle and attribute handle
117        self.indication_semaphores = defaultdict(lambda: asyncio.Semaphore(1))
118        self.pending_confirmations = defaultdict(lambda: None)
119
120    def __str__(self) -> str:
121        return "\n".join(map(str, self.attributes))
122
123    def send_gatt_pdu(self, connection_handle: int, pdu: bytes) -> None:
124        self.device.send_l2cap_pdu(connection_handle, ATT_CID, pdu)
125
126    def next_handle(self) -> int:
127        return 1 + len(self.attributes)
128
129    def get_advertising_service_data(self) -> Dict[Attribute, bytes]:
130        return {
131            attribute: data
132            for attribute in self.attributes
133            if isinstance(attribute, Service)
134            and (data := attribute.get_advertising_data())
135        }
136
137    def get_attribute(self, handle: int) -> Optional[Attribute]:
138        attribute = self.attributes_by_handle.get(handle)
139        if attribute:
140            return attribute
141
142        # Not in the cached map, perform a linear lookup
143        for attribute in self.attributes:
144            if attribute.handle == handle:
145                # Store in cached map
146                self.attributes_by_handle[handle] = attribute
147                return attribute
148        return None
149
150    AttributeGroupType = TypeVar('AttributeGroupType', Service, Characteristic)
151
152    def get_attribute_group(
153        self, handle: int, group_type: Type[AttributeGroupType]
154    ) -> Optional[AttributeGroupType]:
155        return next(
156            (
157                attribute
158                for attribute in self.attributes
159                if isinstance(attribute, group_type)
160                and attribute.handle <= handle <= attribute.end_group_handle
161            ),
162            None,
163        )
164
165    def get_service_attribute(self, service_uuid: UUID) -> Optional[Service]:
166        return next(
167            (
168                attribute
169                for attribute in self.attributes
170                if attribute.type == GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE
171                and isinstance(attribute, Service)
172                and attribute.uuid == service_uuid
173            ),
174            None,
175        )
176
177    def get_characteristic_attributes(
178        self, service_uuid: UUID, characteristic_uuid: UUID
179    ) -> Optional[Tuple[CharacteristicDeclaration, Characteristic]]:
180        service_handle = self.get_service_attribute(service_uuid)
181        if not service_handle:
182            return None
183
184        return next(
185            (
186                (
187                    attribute,
188                    self.get_attribute(attribute.characteristic.handle),
189                )  # type: ignore
190                for attribute in map(
191                    self.get_attribute,
192                    range(service_handle.handle, service_handle.end_group_handle + 1),
193                )
194                if attribute is not None
195                and attribute.type == GATT_CHARACTERISTIC_ATTRIBUTE_TYPE
196                and isinstance(attribute, CharacteristicDeclaration)
197                and attribute.characteristic.uuid == characteristic_uuid
198            ),
199            None,
200        )
201
202    def get_descriptor_attribute(
203        self, service_uuid: UUID, characteristic_uuid: UUID, descriptor_uuid: UUID
204    ) -> Optional[Descriptor]:
205        characteristics = self.get_characteristic_attributes(
206            service_uuid, characteristic_uuid
207        )
208        if not characteristics:
209            return None
210
211        (_, characteristic_value) = characteristics
212
213        return next(
214            (
215                attribute  # type: ignore
216                for attribute in map(
217                    self.get_attribute,
218                    range(
219                        characteristic_value.handle + 1,
220                        characteristic_value.end_group_handle + 1,
221                    ),
222                )
223                if attribute is not None and attribute.type == descriptor_uuid
224            ),
225            None,
226        )
227
228    def add_attribute(self, attribute: Attribute) -> None:
229        # Assign a handle to this attribute
230        attribute.handle = self.next_handle()
231        attribute.end_group_handle = (
232            attribute.handle
233        )  # TODO: keep track of descriptors in the group
234
235        # Add this attribute to the list
236        self.attributes.append(attribute)
237
238    def add_service(self, service: Service) -> None:
239        # Add the service attribute to the DB
240        self.add_attribute(service)
241
242        # Add all included service
243        for included_service in service.included_services:
244            # Not registered yet, register the included service first.
245            if included_service not in self.services:
246                self.add_service(included_service)
247                # TODO: Handle circular service reference
248            include_declaration = IncludedServiceDeclaration(included_service)
249            self.add_attribute(include_declaration)
250
251        # Add all characteristics
252        for characteristic in service.characteristics:
253            # Add a Characteristic Declaration
254            characteristic_declaration = CharacteristicDeclaration(
255                characteristic, self.next_handle() + 1
256            )
257            self.add_attribute(characteristic_declaration)
258
259            # Add the characteristic value
260            self.add_attribute(characteristic)
261
262            # Add the descriptors
263            for descriptor in characteristic.descriptors:
264                self.add_attribute(descriptor)
265
266            # If the characteristic supports subscriptions, add a CCCD descriptor
267            # unless there is one already
268            if (
269                characteristic.properties
270                & (
271                    Characteristic.Properties.NOTIFY
272                    | Characteristic.Properties.INDICATE
273                )
274                and characteristic.get_descriptor(
275                    GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR
276                )
277                is None
278            ):
279                self.add_attribute(
280                    # pylint: disable=line-too-long
281                    Descriptor(
282                        GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
283                        Attribute.READABLE | Attribute.WRITEABLE,
284                        CharacteristicValue(
285                            read=lambda connection, characteristic=characteristic: self.read_cccd(
286                                connection, characteristic
287                            ),
288                            write=lambda connection, value, characteristic=characteristic: self.write_cccd(
289                                connection, characteristic, value
290                            ),
291                        ),
292                    )
293                )
294
295            # Update the service and characteristic group ends
296            characteristic_declaration.end_group_handle = self.attributes[-1].handle
297            characteristic.end_group_handle = self.attributes[-1].handle
298
299        # Update the service group end
300        service.end_group_handle = self.attributes[-1].handle
301        self.services.append(service)
302
303    def add_services(self, services: Iterable[Service]) -> None:
304        for service in services:
305            self.add_service(service)
306
307    def read_cccd(
308        self, connection: Optional[Connection], characteristic: Characteristic
309    ) -> bytes:
310        if connection is None:
311            return bytes([0, 0])
312
313        subscribers = self.subscribers.get(connection.handle)
314        cccd = None
315        if subscribers:
316            cccd = subscribers.get(characteristic.handle)
317
318        return cccd or bytes([0, 0])
319
320    def write_cccd(
321        self,
322        connection: Connection,
323        characteristic: Characteristic,
324        value: bytes,
325    ) -> None:
326        logger.debug(
327            f'Subscription update for connection=0x{connection.handle:04X}, '
328            f'handle=0x{characteristic.handle:04X}: {value.hex()}'
329        )
330
331        # Check parameters
332        if len(value) != 2:
333            logger.warning('CCCD value not 2 bytes long')
334            return
335
336        cccds = self.subscribers.setdefault(connection.handle, {})
337        cccds[characteristic.handle] = value
338        logger.debug(f'CCCDs: {cccds}')
339        notify_enabled = value[0] & 0x01 != 0
340        indicate_enabled = value[0] & 0x02 != 0
341        characteristic.emit(
342            'subscription', connection, notify_enabled, indicate_enabled
343        )
344        self.emit(
345            'characteristic_subscription',
346            connection,
347            characteristic,
348            notify_enabled,
349            indicate_enabled,
350        )
351
352    def send_response(self, connection: Connection, response: ATT_PDU) -> None:
353        logger.debug(
354            f'GATT Response from server: [0x{connection.handle:04X}] {response}'
355        )
356        self.send_gatt_pdu(connection.handle, response.to_bytes())
357
358    async def notify_subscriber(
359        self,
360        connection: Connection,
361        attribute: Attribute,
362        value: Optional[bytes] = None,
363        force: bool = False,
364    ) -> None:
365        # Check if there's a subscriber
366        if not force:
367            subscribers = self.subscribers.get(connection.handle)
368            if not subscribers:
369                logger.debug('not notifying, no subscribers')
370                return
371            cccd = subscribers.get(attribute.handle)
372            if not cccd:
373                logger.debug(
374                    f'not notifying, no subscribers for handle {attribute.handle:04X}'
375                )
376                return
377            if len(cccd) != 2 or (cccd[0] & 0x01 == 0):
378                logger.debug(f'not notifying, cccd={cccd.hex()}')
379                return
380
381        # Get or encode the value
382        value = (
383            await attribute.read_value(connection)
384            if value is None
385            else attribute.encode_value(value)
386        )
387
388        # Truncate if needed
389        if len(value) > connection.att_mtu - 3:
390            value = value[: connection.att_mtu - 3]
391
392        # Notify
393        notification = ATT_Handle_Value_Notification(
394            attribute_handle=attribute.handle, attribute_value=value
395        )
396        logger.debug(
397            f'GATT Notify from server: [0x{connection.handle:04X}] {notification}'
398        )
399        self.send_gatt_pdu(connection.handle, bytes(notification))
400
401    async def indicate_subscriber(
402        self,
403        connection: Connection,
404        attribute: Attribute,
405        value: Optional[bytes] = None,
406        force: bool = False,
407    ) -> None:
408        # Check if there's a subscriber
409        if not force:
410            subscribers = self.subscribers.get(connection.handle)
411            if not subscribers:
412                logger.debug('not indicating, no subscribers')
413                return
414            cccd = subscribers.get(attribute.handle)
415            if not cccd:
416                logger.debug(
417                    f'not indicating, no subscribers for handle {attribute.handle:04X}'
418                )
419                return
420            if len(cccd) != 2 or (cccd[0] & 0x02 == 0):
421                logger.debug(f'not indicating, cccd={cccd.hex()}')
422                return
423
424        # Get or encode the value
425        value = (
426            await attribute.read_value(connection)
427            if value is None
428            else attribute.encode_value(value)
429        )
430
431        # Truncate if needed
432        if len(value) > connection.att_mtu - 3:
433            value = value[: connection.att_mtu - 3]
434
435        # Indicate
436        indication = ATT_Handle_Value_Indication(
437            attribute_handle=attribute.handle, attribute_value=value
438        )
439        logger.debug(
440            f'GATT Indicate from server: [0x{connection.handle:04X}] {indication}'
441        )
442
443        # Wait until we can send (only one pending indication at a time per connection)
444        async with self.indication_semaphores[connection.handle]:
445            assert self.pending_confirmations[connection.handle] is None
446
447            # Create a future value to hold the eventual response
448            pending_confirmation = self.pending_confirmations[connection.handle] = (
449                asyncio.get_running_loop().create_future()
450            )
451
452            try:
453                self.send_gatt_pdu(connection.handle, indication.to_bytes())
454                await asyncio.wait_for(pending_confirmation, GATT_REQUEST_TIMEOUT)
455            except asyncio.TimeoutError as error:
456                logger.warning(color('!!! GATT Indicate timeout', 'red'))
457                raise TimeoutError(f'GATT timeout for {indication.name}') from error
458            finally:
459                self.pending_confirmations[connection.handle] = None
460
461    async def notify_or_indicate_subscribers(
462        self,
463        indicate: bool,
464        attribute: Attribute,
465        value: Optional[bytes] = None,
466        force: bool = False,
467    ) -> None:
468        # Get all the connections for which there's at least one subscription
469        connections = [
470            connection
471            for connection in [
472                self.device.lookup_connection(connection_handle)
473                for (connection_handle, subscribers) in self.subscribers.items()
474                if force or subscribers.get(attribute.handle)
475            ]
476            if connection is not None
477        ]
478
479        # Indicate or notify for each connection
480        if connections:
481            coroutine = self.indicate_subscriber if indicate else self.notify_subscriber
482            await asyncio.wait(
483                [
484                    asyncio.create_task(coroutine(connection, attribute, value, force))
485                    for connection in connections
486                ]
487            )
488
489    async def notify_subscribers(
490        self,
491        attribute: Attribute,
492        value: Optional[bytes] = None,
493        force: bool = False,
494    ):
495        return await self.notify_or_indicate_subscribers(False, attribute, value, force)
496
497    async def indicate_subscribers(
498        self,
499        attribute: Attribute,
500        value: Optional[bytes] = None,
501        force: bool = False,
502    ):
503        return await self.notify_or_indicate_subscribers(True, attribute, value, force)
504
505    def on_disconnection(self, connection: Connection) -> None:
506        if connection.handle in self.subscribers:
507            del self.subscribers[connection.handle]
508        if connection.handle in self.indication_semaphores:
509            del self.indication_semaphores[connection.handle]
510        if connection.handle in self.pending_confirmations:
511            del self.pending_confirmations[connection.handle]
512
513    def on_gatt_pdu(self, connection: Connection, att_pdu: ATT_PDU) -> None:
514        logger.debug(f'GATT Request to server: [0x{connection.handle:04X}] {att_pdu}')
515        handler_name = f'on_{att_pdu.name.lower()}'
516        handler = getattr(self, handler_name, None)
517        if handler is not None:
518            try:
519                handler(connection, att_pdu)
520            except ATT_Error as error:
521                logger.debug(f'normal exception returned by handler: {error}')
522                response = ATT_Error_Response(
523                    request_opcode_in_error=att_pdu.op_code,
524                    attribute_handle_in_error=error.att_handle,
525                    error_code=error.error_code,
526                )
527                self.send_response(connection, response)
528            except Exception as error:
529                logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
530                response = ATT_Error_Response(
531                    request_opcode_in_error=att_pdu.op_code,
532                    attribute_handle_in_error=0x0000,
533                    error_code=ATT_UNLIKELY_ERROR_ERROR,
534                )
535                self.send_response(connection, response)
536                raise error
537        else:
538            # No specific handler registered
539            if att_pdu.op_code in ATT_REQUESTS:
540                # Invoke the generic handler
541                self.on_att_request(connection, att_pdu)
542            else:
543                # Just ignore
544                logger.warning(
545                    color(
546                        f'--- Ignoring GATT Request from [0x{connection.handle:04X}]: ',
547                        'red',
548                    )
549                    + str(att_pdu)
550                )
551
552    #######################################################
553    # ATT handlers
554    #######################################################
555    def on_att_request(self, connection: Connection, pdu: ATT_PDU) -> None:
556        '''
557        Handler for requests without a more specific handler
558        '''
559        logger.warning(
560            color(
561                f'--- Unsupported ATT Request from [0x{connection.handle:04X}]: ', 'red'
562            )
563            + str(pdu)
564        )
565        response = ATT_Error_Response(
566            request_opcode_in_error=pdu.op_code,
567            attribute_handle_in_error=0x0000,
568            error_code=ATT_REQUEST_NOT_SUPPORTED_ERROR,
569        )
570        self.send_response(connection, response)
571
572    def on_att_exchange_mtu_request(self, connection, request):
573        '''
574        See Bluetooth spec Vol 3, Part F - 3.4.2.1 Exchange MTU Request
575        '''
576        self.send_response(
577            connection, ATT_Exchange_MTU_Response(server_rx_mtu=self.max_mtu)
578        )
579
580        # Compute the final MTU
581        if request.client_rx_mtu >= ATT_DEFAULT_MTU:
582            mtu = min(self.max_mtu, request.client_rx_mtu)
583
584            # Notify the device
585            self.device.on_connection_att_mtu_update(connection.handle, mtu)
586        else:
587            logger.warning('invalid client_rx_mtu received, MTU not changed')
588
589    def on_att_find_information_request(self, connection, request):
590        '''
591        See Bluetooth spec Vol 3, Part F - 3.4.3.1 Find Information Request
592        '''
593
594        # Check the request parameters
595        if (
596            request.starting_handle == 0
597            or request.starting_handle > request.ending_handle
598        ):
599            self.send_response(
600                connection,
601                ATT_Error_Response(
602                    request_opcode_in_error=request.op_code,
603                    attribute_handle_in_error=request.starting_handle,
604                    error_code=ATT_INVALID_HANDLE_ERROR,
605                ),
606            )
607            return
608
609        # Build list of returned attributes
610        pdu_space_available = connection.att_mtu - 2
611        attributes = []
612        uuid_size = 0
613        for attribute in (
614            attribute
615            for attribute in self.attributes
616            if attribute.handle >= request.starting_handle
617            and attribute.handle <= request.ending_handle
618        ):
619            this_uuid_size = len(attribute.type.to_pdu_bytes())
620
621            if attributes:
622                # Check if this attribute has the same type size as the previous one
623                if this_uuid_size != uuid_size:
624                    break
625
626            # Check if there's enough space for one more entry
627            uuid_size = this_uuid_size
628            if pdu_space_available < 2 + uuid_size:
629                break
630
631            # Add the attribute to the list
632            attributes.append(attribute)
633            pdu_space_available -= 2 + uuid_size
634
635        # Return the list of attributes
636        if attributes:
637            information_data_list = [
638                struct.pack('<H', attribute.handle) + attribute.type.to_pdu_bytes()
639                for attribute in attributes
640            ]
641            response = ATT_Find_Information_Response(
642                format=1 if len(attributes[0].type.to_pdu_bytes()) == 2 else 2,
643                information_data=b''.join(information_data_list),
644            )
645        else:
646            response = ATT_Error_Response(
647                request_opcode_in_error=request.op_code,
648                attribute_handle_in_error=request.starting_handle,
649                error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
650            )
651
652        self.send_response(connection, response)
653
654    @AsyncRunner.run_in_task()
655    async def on_att_find_by_type_value_request(self, connection, request):
656        '''
657        See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
658        '''
659
660        # Build list of returned attributes
661        pdu_space_available = connection.att_mtu - 2
662        attributes = []
663        async for attribute in (
664            attribute
665            for attribute in self.attributes
666            if attribute.handle >= request.starting_handle
667            and attribute.handle <= request.ending_handle
668            and attribute.type == request.attribute_type
669            and (await attribute.read_value(connection)) == request.attribute_value
670            and pdu_space_available >= 4
671        ):
672            # TODO: check permissions
673
674            # Add the attribute to the list
675            attributes.append(attribute)
676            pdu_space_available -= 4
677
678        # Return the list of attributes
679        if attributes:
680            handles_information_list = []
681            for attribute in attributes:
682                if attribute.type in (
683                    GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
684                    GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
685                    GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
686                ):
687                    # Part of a group
688                    group_end_handle = attribute.end_group_handle
689                else:
690                    # Not part of a group
691                    group_end_handle = attribute.handle
692                handles_information_list.append(
693                    struct.pack('<HH', attribute.handle, group_end_handle)
694                )
695            response = ATT_Find_By_Type_Value_Response(
696                handles_information_list=b''.join(handles_information_list)
697            )
698        else:
699            response = ATT_Error_Response(
700                request_opcode_in_error=request.op_code,
701                attribute_handle_in_error=request.starting_handle,
702                error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
703            )
704
705        self.send_response(connection, response)
706
707    @AsyncRunner.run_in_task()
708    async def on_att_read_by_type_request(self, connection, request):
709        '''
710        See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
711        '''
712
713        pdu_space_available = connection.att_mtu - 2
714
715        response = ATT_Error_Response(
716            request_opcode_in_error=request.op_code,
717            attribute_handle_in_error=request.starting_handle,
718            error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
719        )
720
721        attributes = []
722        for attribute in (
723            attribute
724            for attribute in self.attributes
725            if attribute.type == request.attribute_type
726            and attribute.handle >= request.starting_handle
727            and attribute.handle <= request.ending_handle
728            and pdu_space_available
729        ):
730            try:
731                attribute_value = await attribute.read_value(connection)
732            except ATT_Error as error:
733                # If the first attribute is unreadable, return an error
734                # Otherwise return attributes up to this point
735                if not attributes:
736                    response = ATT_Error_Response(
737                        request_opcode_in_error=request.op_code,
738                        attribute_handle_in_error=attribute.handle,
739                        error_code=error.error_code,
740                    )
741                break
742
743            # Check the attribute value size
744            max_attribute_size = min(connection.att_mtu - 4, 253)
745            if len(attribute_value) > max_attribute_size:
746                # We need to truncate
747                attribute_value = attribute_value[:max_attribute_size]
748            if attributes and len(attributes[0][1]) != len(attribute_value):
749                # Not the same size as previous attribute, stop here
750                break
751
752            # Check if there is enough space
753            entry_size = 2 + len(attribute_value)
754            if pdu_space_available < entry_size:
755                break
756
757            # Add the attribute to the list
758            attributes.append((attribute.handle, attribute_value))
759            pdu_space_available -= entry_size
760
761        if attributes:
762            attribute_data_list = [
763                struct.pack('<H', handle) + value for handle, value in attributes
764            ]
765            response = ATT_Read_By_Type_Response(
766                length=entry_size, attribute_data_list=b''.join(attribute_data_list)
767            )
768        else:
769            logging.debug(f"not found {request}")
770
771        self.send_response(connection, response)
772
773    @AsyncRunner.run_in_task()
774    async def on_att_read_request(self, connection, request):
775        '''
776        See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
777        '''
778
779        if attribute := self.get_attribute(request.attribute_handle):
780            try:
781                value = await attribute.read_value(connection)
782            except ATT_Error as error:
783                response = ATT_Error_Response(
784                    request_opcode_in_error=request.op_code,
785                    attribute_handle_in_error=request.attribute_handle,
786                    error_code=error.error_code,
787                )
788            else:
789                value_size = min(connection.att_mtu - 1, len(value))
790                response = ATT_Read_Response(attribute_value=value[:value_size])
791        else:
792            response = ATT_Error_Response(
793                request_opcode_in_error=request.op_code,
794                attribute_handle_in_error=request.attribute_handle,
795                error_code=ATT_INVALID_HANDLE_ERROR,
796            )
797        self.send_response(connection, response)
798
799    @AsyncRunner.run_in_task()
800    async def on_att_read_blob_request(self, connection, request):
801        '''
802        See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
803        '''
804
805        if attribute := self.get_attribute(request.attribute_handle):
806            try:
807                value = await attribute.read_value(connection)
808            except ATT_Error as error:
809                response = ATT_Error_Response(
810                    request_opcode_in_error=request.op_code,
811                    attribute_handle_in_error=request.attribute_handle,
812                    error_code=error.error_code,
813                )
814            else:
815                if request.value_offset > len(value):
816                    response = ATT_Error_Response(
817                        request_opcode_in_error=request.op_code,
818                        attribute_handle_in_error=request.attribute_handle,
819                        error_code=ATT_INVALID_OFFSET_ERROR,
820                    )
821                elif len(value) <= connection.att_mtu - 1:
822                    response = ATT_Error_Response(
823                        request_opcode_in_error=request.op_code,
824                        attribute_handle_in_error=request.attribute_handle,
825                        error_code=ATT_ATTRIBUTE_NOT_LONG_ERROR,
826                    )
827                else:
828                    part_size = min(
829                        connection.att_mtu - 1, len(value) - request.value_offset
830                    )
831                    response = ATT_Read_Blob_Response(
832                        part_attribute_value=value[
833                            request.value_offset : request.value_offset + part_size
834                        ]
835                    )
836        else:
837            response = ATT_Error_Response(
838                request_opcode_in_error=request.op_code,
839                attribute_handle_in_error=request.attribute_handle,
840                error_code=ATT_INVALID_HANDLE_ERROR,
841            )
842        self.send_response(connection, response)
843
844    @AsyncRunner.run_in_task()
845    async def on_att_read_by_group_type_request(self, connection, request):
846        '''
847        See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
848        '''
849        if request.attribute_group_type not in (
850            GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
851            GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
852        ):
853            response = ATT_Error_Response(
854                request_opcode_in_error=request.op_code,
855                attribute_handle_in_error=request.starting_handle,
856                error_code=ATT_UNSUPPORTED_GROUP_TYPE_ERROR,
857            )
858            self.send_response(connection, response)
859            return
860
861        pdu_space_available = connection.att_mtu - 2
862        attributes = []
863        for attribute in (
864            attribute
865            for attribute in self.attributes
866            if attribute.type == request.attribute_group_type
867            and attribute.handle >= request.starting_handle
868            and attribute.handle <= request.ending_handle
869            and pdu_space_available
870        ):
871            # No need to catch permission errors here, since these attributes
872            # must all be world-readable
873            attribute_value = await attribute.read_value(connection)
874            # Check the attribute value size
875            max_attribute_size = min(connection.att_mtu - 6, 251)
876            if len(attribute_value) > max_attribute_size:
877                # We need to truncate
878                attribute_value = attribute_value[:max_attribute_size]
879            if attributes and len(attributes[0][2]) != len(attribute_value):
880                # Not the same size as previous attributes, stop here
881                break
882
883            # Check if there is enough space
884            entry_size = 4 + len(attribute_value)
885            if pdu_space_available < entry_size:
886                break
887
888            # Add the attribute to the list
889            attributes.append(
890                (attribute.handle, attribute.end_group_handle, attribute_value)
891            )
892            pdu_space_available -= entry_size
893
894        if attributes:
895            attribute_data_list = [
896                struct.pack('<HH', handle, end_group_handle) + value
897                for handle, end_group_handle, value in attributes
898            ]
899            response = ATT_Read_By_Group_Type_Response(
900                length=len(attribute_data_list[0]),
901                attribute_data_list=b''.join(attribute_data_list),
902            )
903        else:
904            response = ATT_Error_Response(
905                request_opcode_in_error=request.op_code,
906                attribute_handle_in_error=request.starting_handle,
907                error_code=ATT_ATTRIBUTE_NOT_FOUND_ERROR,
908            )
909
910        self.send_response(connection, response)
911
912    @AsyncRunner.run_in_task()
913    async def on_att_write_request(self, connection, request):
914        '''
915        See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
916        '''
917
918        # Check that the attribute exists
919        attribute = self.get_attribute(request.attribute_handle)
920        if attribute is None:
921            self.send_response(
922                connection,
923                ATT_Error_Response(
924                    request_opcode_in_error=request.op_code,
925                    attribute_handle_in_error=request.attribute_handle,
926                    error_code=ATT_INVALID_HANDLE_ERROR,
927                ),
928            )
929            return
930
931        # TODO: check permissions
932
933        # Check the request parameters
934        if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE:
935            self.send_response(
936                connection,
937                ATT_Error_Response(
938                    request_opcode_in_error=request.op_code,
939                    attribute_handle_in_error=request.attribute_handle,
940                    error_code=ATT_INVALID_ATTRIBUTE_LENGTH_ERROR,
941                ),
942            )
943            return
944
945        try:
946            # Accept the value
947            await attribute.write_value(connection, request.attribute_value)
948        except ATT_Error as error:
949            response = ATT_Error_Response(
950                request_opcode_in_error=request.op_code,
951                attribute_handle_in_error=request.attribute_handle,
952                error_code=error.error_code,
953            )
954        else:
955            # Done
956            response = ATT_Write_Response()
957        self.send_response(connection, response)
958
959    @AsyncRunner.run_in_task()
960    async def on_att_write_command(self, connection, request):
961        '''
962        See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
963        '''
964
965        # Check that the attribute exists
966        attribute = self.get_attribute(request.attribute_handle)
967        if attribute is None:
968            return
969
970        # TODO: check permissions
971
972        # Check the request parameters
973        if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE:
974            return
975
976        # Accept the value
977        try:
978            await attribute.write_value(connection, request.attribute_value)
979        except Exception as error:
980            logger.exception(f'!!! ignoring exception: {error}')
981
982    def on_att_handle_value_confirmation(self, connection, _confirmation):
983        '''
984        See Bluetooth spec Vol 3, Part F - 3.4.7.3 Handle Value Confirmation
985        '''
986        if self.pending_confirmations[connection.handle] is None:
987            # Not expected!
988            logger.warning(
989                '!!! unexpected confirmation, there is no pending indication'
990            )
991            return
992
993        self.pending_confirmations[connection.handle].set_result(None)
994