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