bumble 0.0.179__py3-none-any.whl → 0.0.181__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bumble/gatt_client.py CHANGED
@@ -38,6 +38,7 @@ from typing import (
38
38
  Any,
39
39
  Iterable,
40
40
  Type,
41
+ Set,
41
42
  TYPE_CHECKING,
42
43
  )
43
44
 
@@ -128,7 +129,7 @@ class ServiceProxy(AttributeProxy):
128
129
  included_services: List[ServiceProxy]
129
130
 
130
131
  @staticmethod
131
- def from_client(service_class, client, service_uuid):
132
+ def from_client(service_class, client: Client, service_uuid: UUID):
132
133
  # The service and its characteristics are considered to have already been
133
134
  # discovered
134
135
  services = client.get_services_by_uuid(service_uuid)
@@ -206,11 +207,11 @@ class CharacteristicProxy(AttributeProxy):
206
207
 
207
208
  return await self.client.subscribe(self, subscriber, prefer_notify)
208
209
 
209
- async def unsubscribe(self, subscriber=None):
210
+ async def unsubscribe(self, subscriber=None, force=False):
210
211
  if subscriber in self.subscribers:
211
212
  subscriber = self.subscribers.pop(subscriber)
212
213
 
213
- return await self.client.unsubscribe(self, subscriber)
214
+ return await self.client.unsubscribe(self, subscriber, force)
214
215
 
215
216
  def __str__(self) -> str:
216
217
  return (
@@ -246,8 +247,12 @@ class ProfileServiceProxy:
246
247
  class Client:
247
248
  services: List[ServiceProxy]
248
249
  cached_values: Dict[int, Tuple[datetime, bytes]]
249
- notification_subscribers: Dict[int, Callable[[bytes], Any]]
250
- indication_subscribers: Dict[int, Callable[[bytes], Any]]
250
+ notification_subscribers: Dict[
251
+ int, Set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
252
+ ]
253
+ indication_subscribers: Dict[
254
+ int, Set[Union[CharacteristicProxy, Callable[[bytes], Any]]]
255
+ ]
251
256
  pending_response: Optional[asyncio.futures.Future[ATT_PDU]]
252
257
  pending_request: Optional[ATT_PDU]
253
258
 
@@ -257,10 +262,8 @@ class Client:
257
262
  self.request_semaphore = asyncio.Semaphore(1)
258
263
  self.pending_request = None
259
264
  self.pending_response = None
260
- self.notification_subscribers = (
261
- {}
262
- ) # Notification subscribers, by attribute handle
263
- self.indication_subscribers = {} # Indication subscribers, by attribute handle
265
+ self.notification_subscribers = {} # Subscriber set, by attribute handle
266
+ self.indication_subscribers = {} # Subscriber set, by attribute handle
264
267
  self.services = []
265
268
  self.cached_values = {}
266
269
 
@@ -682,8 +685,8 @@ class Client:
682
685
  async def discover_descriptors(
683
686
  self,
684
687
  characteristic: Optional[CharacteristicProxy] = None,
685
- start_handle=None,
686
- end_handle=None,
688
+ start_handle: Optional[int] = None,
689
+ end_handle: Optional[int] = None,
687
690
  ) -> List[DescriptorProxy]:
688
691
  '''
689
692
  See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
@@ -789,7 +792,12 @@ class Client:
789
792
 
790
793
  return attributes
791
794
 
792
- async def subscribe(self, characteristic, subscriber=None, prefer_notify=True):
795
+ async def subscribe(
796
+ self,
797
+ characteristic: CharacteristicProxy,
798
+ subscriber: Optional[Callable[[bytes], Any]] = None,
799
+ prefer_notify: bool = True,
800
+ ) -> None:
793
801
  # If we haven't already discovered the descriptors for this characteristic,
794
802
  # do it now
795
803
  if not characteristic.descriptors_discovered:
@@ -826,6 +834,7 @@ class Client:
826
834
  subscriber_set = subscribers.setdefault(characteristic.handle, set())
827
835
  if subscriber is not None:
828
836
  subscriber_set.add(subscriber)
837
+
829
838
  # Add the characteristic as a subscriber, which will result in the
830
839
  # characteristic emitting an 'update' event when a notification or indication
831
840
  # is received
@@ -833,7 +842,18 @@ class Client:
833
842
 
834
843
  await self.write_value(cccd, struct.pack('<H', bits), with_response=True)
835
844
 
836
- async def unsubscribe(self, characteristic, subscriber=None):
845
+ async def unsubscribe(
846
+ self,
847
+ characteristic: CharacteristicProxy,
848
+ subscriber: Optional[Callable[[bytes], Any]] = None,
849
+ force: bool = False,
850
+ ) -> None:
851
+ '''
852
+ Unsubscribe from a characteristic.
853
+
854
+ If `force` is True, this will write zeros to the CCCD when there are no
855
+ subscribers left, even if there were already no registered subscribers.
856
+ '''
837
857
  # If we haven't already discovered the descriptors for this characteristic,
838
858
  # do it now
839
859
  if not characteristic.descriptors_discovered:
@@ -847,31 +867,45 @@ class Client:
847
867
  logger.warning('unsubscribing from characteristic with no CCCD descriptor')
848
868
  return
849
869
 
870
+ # Check if the characteristic has subscribers
871
+ if not (
872
+ characteristic.handle in self.notification_subscribers
873
+ or characteristic.handle in self.indication_subscribers
874
+ ):
875
+ if not force:
876
+ return
877
+
878
+ # Remove the subscriber(s)
850
879
  if subscriber is not None:
851
880
  # Remove matching subscriber from subscriber sets
852
881
  for subscriber_set in (
853
882
  self.notification_subscribers,
854
883
  self.indication_subscribers,
855
884
  ):
856
- subscribers = subscriber_set.get(characteristic.handle, [])
857
- if subscriber in subscribers:
885
+ if (
886
+ subscribers := subscriber_set.get(characteristic.handle)
887
+ ) and subscriber in subscribers:
858
888
  subscribers.remove(subscriber)
859
889
 
860
890
  # Cleanup if we removed the last one
861
891
  if not subscribers:
862
892
  del subscriber_set[characteristic.handle]
863
893
  else:
864
- # Remove all subscribers for this attribute from the sets!
894
+ # Remove all subscribers for this attribute from the sets
865
895
  self.notification_subscribers.pop(characteristic.handle, None)
866
896
  self.indication_subscribers.pop(characteristic.handle, None)
867
897
 
868
- if not self.notification_subscribers and not self.indication_subscribers:
898
+ # Update the CCCD
899
+ if not (
900
+ characteristic.handle in self.notification_subscribers
901
+ or characteristic.handle in self.indication_subscribers
902
+ ):
869
903
  # No more subscribers left
870
904
  await self.write_value(cccd, b'\x00\x00', with_response=True)
871
905
 
872
906
  async def read_value(
873
907
  self, attribute: Union[int, AttributeProxy], no_long_read: bool = False
874
- ) -> Any:
908
+ ) -> bytes:
875
909
  '''
876
910
  See Vol 3, Part G - 4.8.1 Read Characteristic Value
877
911
 
@@ -1067,7 +1101,7 @@ class Client:
1067
1101
  def on_att_handle_value_notification(self, notification):
1068
1102
  # Call all subscribers
1069
1103
  subscribers = self.notification_subscribers.get(
1070
- notification.attribute_handle, []
1104
+ notification.attribute_handle, set()
1071
1105
  )
1072
1106
  if not subscribers:
1073
1107
  logger.warning('!!! received notification with no subscriber')
@@ -1081,7 +1115,9 @@ class Client:
1081
1115
 
1082
1116
  def on_att_handle_value_indication(self, indication):
1083
1117
  # Call all subscribers
1084
- subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
1118
+ subscribers = self.indication_subscribers.get(
1119
+ indication.attribute_handle, set()
1120
+ )
1085
1121
  if not subscribers:
1086
1122
  logger.warning('!!! received indication with no subscriber')
1087
1123
 
bumble/gatt_server.py CHANGED
@@ -31,9 +31,9 @@ import struct
31
31
  from typing import List, Tuple, Optional, TypeVar, Type, Dict, Iterable, TYPE_CHECKING
32
32
  from pyee import EventEmitter
33
33
 
34
- from .colors import color
35
- from .core import UUID
36
- from .att import (
34
+ from bumble.colors import color
35
+ from bumble.core import UUID
36
+ from bumble.att import (
37
37
  ATT_ATTRIBUTE_NOT_FOUND_ERROR,
38
38
  ATT_ATTRIBUTE_NOT_LONG_ERROR,
39
39
  ATT_CID,
@@ -60,7 +60,7 @@ from .att import (
60
60
  ATT_Write_Response,
61
61
  Attribute,
62
62
  )
63
- from .gatt import (
63
+ from bumble.gatt import (
64
64
  GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
65
65
  GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
66
66
  GATT_MAX_ATTRIBUTE_VALUE_SIZE,
@@ -74,6 +74,7 @@ from .gatt import (
74
74
  Descriptor,
75
75
  Service,
76
76
  )
77
+ from bumble.utils import AsyncRunner
77
78
 
78
79
  if TYPE_CHECKING:
79
80
  from bumble.device import Device, Connection
@@ -379,7 +380,7 @@ class Server(EventEmitter):
379
380
 
380
381
  # Get or encode the value
381
382
  value = (
382
- attribute.read_value(connection)
383
+ await attribute.read_value(connection)
383
384
  if value is None
384
385
  else attribute.encode_value(value)
385
386
  )
@@ -422,7 +423,7 @@ class Server(EventEmitter):
422
423
 
423
424
  # Get or encode the value
424
425
  value = (
425
- attribute.read_value(connection)
426
+ await attribute.read_value(connection)
426
427
  if value is None
427
428
  else attribute.encode_value(value)
428
429
  )
@@ -650,7 +651,8 @@ class Server(EventEmitter):
650
651
 
651
652
  self.send_response(connection, response)
652
653
 
653
- def on_att_find_by_type_value_request(self, connection, request):
654
+ @AsyncRunner.run_in_task()
655
+ async def on_att_find_by_type_value_request(self, connection, request):
654
656
  '''
655
657
  See Bluetooth spec Vol 3, Part F - 3.4.3.3 Find By Type Value Request
656
658
  '''
@@ -658,13 +660,13 @@ class Server(EventEmitter):
658
660
  # Build list of returned attributes
659
661
  pdu_space_available = connection.att_mtu - 2
660
662
  attributes = []
661
- for attribute in (
663
+ async for attribute in (
662
664
  attribute
663
665
  for attribute in self.attributes
664
666
  if attribute.handle >= request.starting_handle
665
667
  and attribute.handle <= request.ending_handle
666
668
  and attribute.type == request.attribute_type
667
- and attribute.read_value(connection) == request.attribute_value
669
+ and (await attribute.read_value(connection)) == request.attribute_value
668
670
  and pdu_space_available >= 4
669
671
  ):
670
672
  # TODO: check permissions
@@ -702,7 +704,8 @@ class Server(EventEmitter):
702
704
 
703
705
  self.send_response(connection, response)
704
706
 
705
- def on_att_read_by_type_request(self, connection, request):
707
+ @AsyncRunner.run_in_task()
708
+ async def on_att_read_by_type_request(self, connection, request):
706
709
  '''
707
710
  See Bluetooth spec Vol 3, Part F - 3.4.4.1 Read By Type Request
708
711
  '''
@@ -725,7 +728,7 @@ class Server(EventEmitter):
725
728
  and pdu_space_available
726
729
  ):
727
730
  try:
728
- attribute_value = attribute.read_value(connection)
731
+ attribute_value = await attribute.read_value(connection)
729
732
  except ATT_Error as error:
730
733
  # If the first attribute is unreadable, return an error
731
734
  # Otherwise return attributes up to this point
@@ -767,14 +770,15 @@ class Server(EventEmitter):
767
770
 
768
771
  self.send_response(connection, response)
769
772
 
770
- def on_att_read_request(self, connection, request):
773
+ @AsyncRunner.run_in_task()
774
+ async def on_att_read_request(self, connection, request):
771
775
  '''
772
776
  See Bluetooth spec Vol 3, Part F - 3.4.4.3 Read Request
773
777
  '''
774
778
 
775
779
  if attribute := self.get_attribute(request.attribute_handle):
776
780
  try:
777
- value = attribute.read_value(connection)
781
+ value = await attribute.read_value(connection)
778
782
  except ATT_Error as error:
779
783
  response = ATT_Error_Response(
780
784
  request_opcode_in_error=request.op_code,
@@ -792,14 +796,15 @@ class Server(EventEmitter):
792
796
  )
793
797
  self.send_response(connection, response)
794
798
 
795
- def on_att_read_blob_request(self, connection, request):
799
+ @AsyncRunner.run_in_task()
800
+ async def on_att_read_blob_request(self, connection, request):
796
801
  '''
797
802
  See Bluetooth spec Vol 3, Part F - 3.4.4.5 Read Blob Request
798
803
  '''
799
804
 
800
805
  if attribute := self.get_attribute(request.attribute_handle):
801
806
  try:
802
- value = attribute.read_value(connection)
807
+ value = await attribute.read_value(connection)
803
808
  except ATT_Error as error:
804
809
  response = ATT_Error_Response(
805
810
  request_opcode_in_error=request.op_code,
@@ -836,7 +841,8 @@ class Server(EventEmitter):
836
841
  )
837
842
  self.send_response(connection, response)
838
843
 
839
- def on_att_read_by_group_type_request(self, connection, request):
844
+ @AsyncRunner.run_in_task()
845
+ async def on_att_read_by_group_type_request(self, connection, request):
840
846
  '''
841
847
  See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request
842
848
  '''
@@ -864,7 +870,7 @@ class Server(EventEmitter):
864
870
  ):
865
871
  # No need to catch permission errors here, since these attributes
866
872
  # must all be world-readable
867
- attribute_value = attribute.read_value(connection)
873
+ attribute_value = await attribute.read_value(connection)
868
874
  # Check the attribute value size
869
875
  max_attribute_size = min(connection.att_mtu - 6, 251)
870
876
  if len(attribute_value) > max_attribute_size:
@@ -903,7 +909,8 @@ class Server(EventEmitter):
903
909
 
904
910
  self.send_response(connection, response)
905
911
 
906
- def on_att_write_request(self, connection, request):
912
+ @AsyncRunner.run_in_task()
913
+ async def on_att_write_request(self, connection, request):
907
914
  '''
908
915
  See Bluetooth spec Vol 3, Part F - 3.4.5.1 Write Request
909
916
  '''
@@ -936,12 +943,13 @@ class Server(EventEmitter):
936
943
  return
937
944
 
938
945
  # Accept the value
939
- attribute.write_value(connection, request.attribute_value)
946
+ await attribute.write_value(connection, request.attribute_value)
940
947
 
941
948
  # Done
942
949
  self.send_response(connection, ATT_Write_Response())
943
950
 
944
- def on_att_write_command(self, connection, request):
951
+ @AsyncRunner.run_in_task()
952
+ async def on_att_write_command(self, connection, request):
945
953
  '''
946
954
  See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command
947
955
  '''
@@ -959,9 +967,9 @@ class Server(EventEmitter):
959
967
 
960
968
  # Accept the value
961
969
  try:
962
- attribute.write_value(connection, request.attribute_value)
970
+ await attribute.write_value(connection, request.attribute_value)
963
971
  except Exception as error:
964
- logger.warning(f'!!! ignoring exception: {error}')
972
+ logger.exception(f'!!! ignoring exception: {error}')
965
973
 
966
974
  def on_att_handle_value_confirmation(self, connection, _confirmation):
967
975
  '''