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