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