bumble 0.0.179__py3-none-any.whl → 0.0.180__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/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.0.179'
16
- __version_tuple__ = version_tuple = (0, 0, 179)
15
+ __version__ = version = '0.0.180'
16
+ __version_tuple__ = version_tuple = (0, 0, 180)
bumble/device.py CHANGED
@@ -23,6 +23,7 @@ import asyncio
23
23
  import logging
24
24
  from contextlib import asynccontextmanager, AsyncExitStack
25
25
  from dataclasses import dataclass
26
+ from collections.abc import Iterable
26
27
  from typing import (
27
28
  Any,
28
29
  Callable,
@@ -32,6 +33,7 @@ from typing import (
32
33
  Optional,
33
34
  Tuple,
34
35
  Type,
36
+ TypeVar,
35
37
  Set,
36
38
  Union,
37
39
  cast,
@@ -440,8 +442,11 @@ class LePhyOptions:
440
442
 
441
443
 
442
444
  # -----------------------------------------------------------------------------
445
+ _PROXY_CLASS = TypeVar('_PROXY_CLASS', bound=gatt_client.ProfileServiceProxy)
446
+
447
+
443
448
  class Peer:
444
- def __init__(self, connection):
449
+ def __init__(self, connection: Connection) -> None:
445
450
  self.connection = connection
446
451
 
447
452
  # Create a GATT client for the connection
@@ -449,77 +454,113 @@ class Peer:
449
454
  connection.gatt_client = self.gatt_client
450
455
 
451
456
  @property
452
- def services(self):
457
+ def services(self) -> List[gatt_client.ServiceProxy]:
453
458
  return self.gatt_client.services
454
459
 
455
- async def request_mtu(self, mtu):
460
+ async def request_mtu(self, mtu: int) -> int:
456
461
  mtu = await self.gatt_client.request_mtu(mtu)
457
462
  self.connection.emit('connection_att_mtu_update')
458
463
  return mtu
459
464
 
460
- async def discover_service(self, uuid):
465
+ async def discover_service(
466
+ self, uuid: Union[core.UUID, str]
467
+ ) -> List[gatt_client.ServiceProxy]:
461
468
  return await self.gatt_client.discover_service(uuid)
462
469
 
463
- async def discover_services(self, uuids=()):
470
+ async def discover_services(
471
+ self, uuids: Iterable[core.UUID] = ()
472
+ ) -> List[gatt_client.ServiceProxy]:
464
473
  return await self.gatt_client.discover_services(uuids)
465
474
 
466
- async def discover_included_services(self, service):
475
+ async def discover_included_services(
476
+ self, service: gatt_client.ServiceProxy
477
+ ) -> List[gatt_client.ServiceProxy]:
467
478
  return await self.gatt_client.discover_included_services(service)
468
479
 
469
- async def discover_characteristics(self, uuids=(), service=None):
480
+ async def discover_characteristics(
481
+ self,
482
+ uuids: Iterable[Union[core.UUID, str]] = (),
483
+ service: Optional[gatt_client.ServiceProxy] = None,
484
+ ) -> List[gatt_client.CharacteristicProxy]:
470
485
  return await self.gatt_client.discover_characteristics(
471
486
  uuids=uuids, service=service
472
487
  )
473
488
 
474
489
  async def discover_descriptors(
475
- self, characteristic=None, start_handle=None, end_handle=None
490
+ self,
491
+ characteristic: Optional[gatt_client.CharacteristicProxy] = None,
492
+ start_handle: Optional[int] = None,
493
+ end_handle: Optional[int] = None,
476
494
  ):
477
495
  return await self.gatt_client.discover_descriptors(
478
496
  characteristic, start_handle, end_handle
479
497
  )
480
498
 
481
- async def discover_attributes(self):
499
+ async def discover_attributes(self) -> List[gatt_client.AttributeProxy]:
482
500
  return await self.gatt_client.discover_attributes()
483
501
 
484
- async def subscribe(self, characteristic, subscriber=None, prefer_notify=True):
502
+ async def subscribe(
503
+ self,
504
+ characteristic: gatt_client.CharacteristicProxy,
505
+ subscriber: Optional[Callable[[bytes], Any]] = None,
506
+ prefer_notify: bool = True,
507
+ ) -> None:
485
508
  return await self.gatt_client.subscribe(
486
509
  characteristic, subscriber, prefer_notify
487
510
  )
488
511
 
489
- async def unsubscribe(self, characteristic, subscriber=None):
512
+ async def unsubscribe(
513
+ self,
514
+ characteristic: gatt_client.CharacteristicProxy,
515
+ subscriber: Optional[Callable[[bytes], Any]] = None,
516
+ ) -> None:
490
517
  return await self.gatt_client.unsubscribe(characteristic, subscriber)
491
518
 
492
- async def read_value(self, attribute):
519
+ async def read_value(
520
+ self, attribute: Union[int, gatt_client.AttributeProxy]
521
+ ) -> bytes:
493
522
  return await self.gatt_client.read_value(attribute)
494
523
 
495
- async def write_value(self, attribute, value, with_response=False):
524
+ async def write_value(
525
+ self,
526
+ attribute: Union[int, gatt_client.AttributeProxy],
527
+ value: bytes,
528
+ with_response: bool = False,
529
+ ) -> None:
496
530
  return await self.gatt_client.write_value(attribute, value, with_response)
497
531
 
498
- async def read_characteristics_by_uuid(self, uuid, service=None):
532
+ async def read_characteristics_by_uuid(
533
+ self, uuid: core.UUID, service: Optional[gatt_client.ServiceProxy] = None
534
+ ) -> List[bytes]:
499
535
  return await self.gatt_client.read_characteristics_by_uuid(uuid, service)
500
536
 
501
- def get_services_by_uuid(self, uuid):
537
+ def get_services_by_uuid(self, uuid: core.UUID) -> List[gatt_client.ServiceProxy]:
502
538
  return self.gatt_client.get_services_by_uuid(uuid)
503
539
 
504
- def get_characteristics_by_uuid(self, uuid, service=None):
540
+ def get_characteristics_by_uuid(
541
+ self, uuid: core.UUID, service: Optional[gatt_client.ServiceProxy] = None
542
+ ) -> List[gatt_client.CharacteristicProxy]:
505
543
  return self.gatt_client.get_characteristics_by_uuid(uuid, service)
506
544
 
507
- def create_service_proxy(self, proxy_class):
508
- return proxy_class.from_client(self.gatt_client)
545
+ def create_service_proxy(self, proxy_class: Type[_PROXY_CLASS]) -> _PROXY_CLASS:
546
+ return cast(_PROXY_CLASS, proxy_class.from_client(self.gatt_client))
509
547
 
510
- async def discover_service_and_create_proxy(self, proxy_class):
548
+ async def discover_service_and_create_proxy(
549
+ self, proxy_class: Type[_PROXY_CLASS]
550
+ ) -> Optional[_PROXY_CLASS]:
511
551
  # Discover the first matching service and its characteristics
512
552
  services = await self.discover_service(proxy_class.SERVICE_CLASS.UUID)
513
553
  if services:
514
554
  service = services[0]
515
555
  await service.discover_characteristics()
516
556
  return self.create_service_proxy(proxy_class)
557
+ return None
517
558
 
518
- async def sustain(self, timeout=None):
559
+ async def sustain(self, timeout: Optional[float] = None) -> None:
519
560
  await self.connection.sustain(timeout)
520
561
 
521
562
  # [Classic only]
522
- async def request_name(self):
563
+ async def request_name(self) -> str:
523
564
  return await self.connection.request_remote_name()
524
565
 
525
566
  async def __aenter__(self):
@@ -532,7 +573,7 @@ class Peer:
532
573
  async def __aexit__(self, exc_type, exc_value, traceback):
533
574
  pass
534
575
 
535
- def __str__(self):
576
+ def __str__(self) -> str:
536
577
  return f'{self.connection.peer_address} as {self.connection.role_name}'
537
578
 
538
579
 
@@ -732,7 +773,7 @@ class Connection(CompositeEventEmitter):
732
773
  async def switch_role(self, role: int) -> None:
733
774
  return await self.device.switch_role(self, role)
734
775
 
735
- async def sustain(self, timeout=None):
776
+ async def sustain(self, timeout: Optional[float] = None) -> None:
736
777
  """Idles the current task waiting for a disconnect or timeout"""
737
778
 
738
779
  abort = asyncio.get_running_loop().create_future()
bumble/gatt.py CHANGED
@@ -93,20 +93,35 @@ GATT_RECONNECTION_CONFIGURATION_SERVICE = UUID.from_16_bits(0x1829, 'Reconne
93
93
  GATT_INSULIN_DELIVERY_SERVICE = UUID.from_16_bits(0x183A, 'Insulin Delivery')
94
94
  GATT_BINARY_SENSOR_SERVICE = UUID.from_16_bits(0x183B, 'Binary Sensor')
95
95
  GATT_EMERGENCY_CONFIGURATION_SERVICE = UUID.from_16_bits(0x183C, 'Emergency Configuration')
96
+ GATT_AUTHORIZATION_CONTROL_SERVICE = UUID.from_16_bits(0x183D, 'Authorization Control')
96
97
  GATT_PHYSICAL_ACTIVITY_MONITOR_SERVICE = UUID.from_16_bits(0x183E, 'Physical Activity Monitor')
98
+ GATT_ELAPSED_TIME_SERVICE = UUID.from_16_bits(0x183F, 'Elapsed Time')
99
+ GATT_GENERIC_HEALTH_SENSOR_SERVICE = UUID.from_16_bits(0x1840, 'Generic Health Sensor')
97
100
  GATT_AUDIO_INPUT_CONTROL_SERVICE = UUID.from_16_bits(0x1843, 'Audio Input Control')
98
101
  GATT_VOLUME_CONTROL_SERVICE = UUID.from_16_bits(0x1844, 'Volume Control')
99
102
  GATT_VOLUME_OFFSET_CONTROL_SERVICE = UUID.from_16_bits(0x1845, 'Volume Offset Control')
100
- GATT_COORDINATED_SET_IDENTIFICATION_SERVICE = UUID.from_16_bits(0x1846, 'Coordinated Set Identification Service')
103
+ GATT_COORDINATED_SET_IDENTIFICATION_SERVICE = UUID.from_16_bits(0x1846, 'Coordinated Set Identification')
101
104
  GATT_DEVICE_TIME_SERVICE = UUID.from_16_bits(0x1847, 'Device Time')
102
- GATT_MEDIA_CONTROL_SERVICE = UUID.from_16_bits(0x1848, 'Media Control Service')
103
- GATT_GENERIC_MEDIA_CONTROL_SERVICE = UUID.from_16_bits(0x1849, 'Generic Media Control Service')
105
+ GATT_MEDIA_CONTROL_SERVICE = UUID.from_16_bits(0x1848, 'Media Control')
106
+ GATT_GENERIC_MEDIA_CONTROL_SERVICE = UUID.from_16_bits(0x1849, 'Generic Media Control')
104
107
  GATT_CONSTANT_TONE_EXTENSION_SERVICE = UUID.from_16_bits(0x184A, 'Constant Tone Extension')
105
- GATT_TELEPHONE_BEARER_SERVICE = UUID.from_16_bits(0x184B, 'Telephone Bearer Service')
106
- GATT_GENERIC_TELEPHONE_BEARER_SERVICE = UUID.from_16_bits(0x184C, 'Generic Telephone Bearer Service')
108
+ GATT_TELEPHONE_BEARER_SERVICE = UUID.from_16_bits(0x184B, 'Telephone Bearer')
109
+ GATT_GENERIC_TELEPHONE_BEARER_SERVICE = UUID.from_16_bits(0x184C, 'Generic Telephone Bearer')
107
110
  GATT_MICROPHONE_CONTROL_SERVICE = UUID.from_16_bits(0x184D, 'Microphone Control')
108
-
109
- # Types
111
+ GATT_AUDIO_STREAM_CONTROL_SERVICE = UUID.from_16_bits(0x184E, 'Audio Stream Control')
112
+ GATT_BROADCAST_AUDIO_SCAN_SERVICE = UUID.from_16_bits(0x184F, 'Broadcast Audio Scan')
113
+ GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE = UUID.from_16_bits(0x1850, 'Published Audio Capabilities')
114
+ GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE = UUID.from_16_bits(0x1851, 'Basic Audio Announcement')
115
+ GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE = UUID.from_16_bits(0x1852, 'Broadcast Audio Announcement')
116
+ GATT_COMMON_AUDIO_SERVICE = UUID.from_16_bits(0x1853, 'Common Audio')
117
+ GATT_HEARING_ACCESS_SERVICE = UUID.from_16_bits(0x1854, 'Hearing Access')
118
+ GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE = UUID.from_16_bits(0x1855, 'Telephony and Media Audio')
119
+ GATT_PUBLIC_BROADCAST_ANNOUNCEMENT_SERVICE = UUID.from_16_bits(0x1856, 'Public Broadcast Announcement')
120
+ GATT_ELECTRONIC_SHELF_LABEL_SERVICE = UUID.from_16_bits(0X1857, 'Electronic Shelf Label')
121
+ GATT_GAMING_AUDIO_SERVICE = UUID.from_16_bits(0x1858, 'Gaming Audio')
122
+ GATT_MESH_PROXY_SOLICITATION_SERVICE = UUID.from_16_bits(0x1859, 'Mesh Audio Solicitation')
123
+
124
+ # Attribute Types
110
125
  GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE = UUID.from_16_bits(0x2800, 'Primary Service')
111
126
  GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE = UUID.from_16_bits(0x2801, 'Secondary Service')
112
127
  GATT_INCLUDE_ATTRIBUTE_TYPE = UUID.from_16_bits(0x2802, 'Include')
@@ -129,6 +144,8 @@ GATT_ENVIRONMENTAL_SENSING_MEASUREMENT_DESCRIPTOR = UUID.from_16_bits(0x290C,
129
144
  GATT_ENVIRONMENTAL_SENSING_TRIGGER_DESCRIPTOR = UUID.from_16_bits(0x290D, 'Environmental Sensing Trigger Setting')
130
145
  GATT_TIME_TRIGGER_DESCRIPTOR = UUID.from_16_bits(0x290E, 'Time Trigger Setting')
131
146
  GATT_COMPLETE_BR_EDR_TRANSPORT_BLOCK_DATA_DESCRIPTOR = UUID.from_16_bits(0x290F, 'Complete BR-EDR Transport Block Data')
147
+ GATT_OBSERVATION_SCHEDULE_DESCRIPTOR = UUID.from_16_bits(0x290F, 'Observation Schedule')
148
+ GATT_VALID_RANGE_AND_ACCURACY_DESCRIPTOR = UUID.from_16_bits(0x290F, 'Valid Range And Accuracy')
132
149
 
133
150
  # Device Information Service
134
151
  GATT_SYSTEM_ID_CHARACTERISTIC = UUID.from_16_bits(0x2A23, 'System ID')
@@ -156,6 +173,96 @@ GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2A39, 'Heart
156
173
  # Battery Service
157
174
  GATT_BATTERY_LEVEL_CHARACTERISTIC = UUID.from_16_bits(0x2A19, 'Battery Level')
158
175
 
176
+ # Telephony And Media Audio Service (TMAS)
177
+ GATT_TMAP_ROLE_CHARACTERISTIC = UUID.from_16_bits(0x2B51, 'TMAP Role')
178
+
179
+ # Audio Input Control Service (AICS)
180
+ GATT_AUDIO_INPUT_STATE_CHARACTERISTIC = UUID.from_16_bits(0x2B77, 'Audio Input State')
181
+ GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC = UUID.from_16_bits(0x2B78, 'Gain Settings Attribute')
182
+ GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC = UUID.from_16_bits(0x2B79, 'Audio Input Type')
183
+ GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC = UUID.from_16_bits(0x2B7A, 'Audio Input Status')
184
+ GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2B7B, 'Audio Input Control Point')
185
+ GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC = UUID.from_16_bits(0x2B7C, 'Audio Input Description')
186
+
187
+ # Volume Control Service (VCS)
188
+ GATT_VOLUME_STATE_CHARACTERISTIC = UUID.from_16_bits(0x2B7D, 'Volume State')
189
+ GATT_VOLUME_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2B7E, 'Volume Control Point')
190
+ GATT_VOLUME_FLAGS_CHARACTERISTIC = UUID.from_16_bits(0x2B7F, 'Volume Flags')
191
+
192
+ # Volume Offset Control Service (VOCS)
193
+ GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC = UUID.from_16_bits(0x2B80, 'Volume Offset State')
194
+ GATT_AUDIO_LOCATION_CHARACTERISTIC = UUID.from_16_bits(0x2B81, 'Audio Location')
195
+ GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2B82, 'Volume Offset Control Point')
196
+ GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC = UUID.from_16_bits(0x2B83, 'Audio Output Description')
197
+
198
+ # Coordinated Set Identification Service (CSIS)
199
+ GATT_SET_IDENTITY_RESOLVING_KEY_CHARACTERISTIC = UUID.from_16_bits(0x2B84, 'Set Identity Resolving Key')
200
+ GATT_COORDINATED_SET_SIZE_CHARACTERISTIC = UUID.from_16_bits(0x2B85, 'Coordinated Set Size')
201
+ GATT_SET_MEMBER_LOCK_CHARACTERISTIC = UUID.from_16_bits(0x2B86, 'Set Member Lock')
202
+ GATT_SET_MEMBER_RANK_CHARACTERISTIC = UUID.from_16_bits(0x2B87, 'Set Member Rank')
203
+
204
+ # Media Control Service (MCS)
205
+ GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2B93, 'Media Player Name')
206
+ GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC = UUID.from_16_bits(0x2B94, 'Media Player Icon Object ID')
207
+ GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC = UUID.from_16_bits(0x2B95, 'Media Player Icon URL')
208
+ GATT_TRACK_CHANGED_CHARACTERISTIC = UUID.from_16_bits(0x2B96, 'Track Changed')
209
+ GATT_TRACK_TITLE_CHARACTERISTIC = UUID.from_16_bits(0x2B97, 'Track Title')
210
+ GATT_TRACK_DURATION_CHARACTERISTIC = UUID.from_16_bits(0x2B98, 'Track Duration')
211
+ GATT_TRACK_POSITION_CHARACTERISTIC = UUID.from_16_bits(0x2B99, 'Track Position')
212
+ GATT_PLAYBACK_SPEED_CHARACTERISTIC = UUID.from_16_bits(0x2B9A, 'Playback Speed')
213
+ GATT_SEEKING_SPEED_CHARACTERISTIC = UUID.from_16_bits(0x2B9B, 'Seeking Speed')
214
+ GATT_CURRENT_TRACK_SEGMENTS_OBJECT_ID_CHARACTERISTIC = UUID.from_16_bits(0x2B9C, 'Current Track Segments Object ID')
215
+ GATT_CURRENT_TRACK_OBJECT_ID_CHARACTERISTIC = UUID.from_16_bits(0x2B9D, 'Current Track Object ID')
216
+ GATT_NEXT_TRACK_OBJECT_ID_CHARACTERISTIC = UUID.from_16_bits(0x2B9E, 'Next Track Object ID')
217
+ GATT_PARENT_GROUP_OBJECT_ID_CHARACTERISTIC = UUID.from_16_bits(0x2B9F, 'Parent Group Object ID')
218
+ GATT_CURRENT_GROUP_OBJECT_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BA0, 'Current Group Object ID')
219
+ GATT_PLAYING_ORDER_CHARACTERISTIC = UUID.from_16_bits(0x2BA1, 'Playing Order')
220
+ GATT_PLAYING_ORDERS_SUPPORTED_CHARACTERISTIC = UUID.from_16_bits(0x2BA2, 'Playing Orders Supported')
221
+ GATT_MEDIA_STATE_CHARACTERISTIC = UUID.from_16_bits(0x2BA3, 'Media State')
222
+ GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BA4, 'Media Control Point')
223
+ GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC = UUID.from_16_bits(0x2BA5, 'Media Control Point Opcodes Supported')
224
+ GATT_SEARCH_RESULTS_OBJECT_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BA6, 'Search Results Object ID')
225
+ GATT_SEARCH_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BA7, 'Search Control Point')
226
+ GATT_CONTENT_CONTROL_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BBA, 'Content Control Id')
227
+
228
+ # Telephone Bearer Service (TBS)
229
+ GATT_BEARER_PROVIDER_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2BB4, 'Bearer Provider Name')
230
+ GATT_BEARER_UCI_CHARACTERISTIC = UUID.from_16_bits(0x2BB5, 'Bearer UCI')
231
+ GATT_BEARER_TECHNOLOGY_CHARACTERISTIC = UUID.from_16_bits(0x2BB6, 'Bearer Technology')
232
+ GATT_BEARER_URI_SCHEMES_SUPPORTED_LIST_CHARACTERISTIC = UUID.from_16_bits(0x2BB7, 'Bearer URI Schemes Supported List')
233
+ GATT_BEARER_SIGNAL_STRENGTH_CHARACTERISTIC = UUID.from_16_bits(0x2BB8, 'Bearer Signal Strength')
234
+ GATT_BEARER_SIGNAL_STRENGTH_REPORTING_INTERVAL_CHARACTERISTIC = UUID.from_16_bits(0x2BB9, 'Bearer Signal Strength Reporting Interval')
235
+ GATT_BEARER_LIST_CURRENT_CALLS_CHARACTERISTIC = UUID.from_16_bits(0x2BBA, 'Bearer List Current Calls')
236
+ GATT_CONTENT_CONTROL_ID_CHARACTERISTIC = UUID.from_16_bits(0x2BBB, 'Content Control ID')
237
+ GATT_STATUS_FLAGS_CHARACTERISTIC = UUID.from_16_bits(0x2BBC, 'Status Flags')
238
+ GATT_INCOMING_CALL_TARGET_BEARER_URI_CHARACTERISTIC = UUID.from_16_bits(0x2BBD, 'Incoming Call Target Bearer URI')
239
+ GATT_CALL_STATE_CHARACTERISTIC = UUID.from_16_bits(0x2BBE, 'Call State')
240
+ GATT_CALL_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BBF, 'Call Control Point')
241
+ GATT_CALL_CONTROL_POINT_OPTIONAL_OPCODES_CHARACTERISTIC = UUID.from_16_bits(0x2BC0, 'Call Control Point Optional Opcodes')
242
+ GATT_TERMINATION_REASON_CHARACTERISTIC = UUID.from_16_bits(0x2BC1, 'Termination Reason')
243
+ GATT_INCOMING_CALL_CHARACTERISTIC = UUID.from_16_bits(0x2BC2, 'Incoming Call')
244
+ GATT_CALL_FRIENDLY_NAME_CHARACTERISTIC = UUID.from_16_bits(0x2BC3, 'Call Friendly Name')
245
+
246
+ # Microphone Control Service (MICS)
247
+ GATT_MUTE_CHARACTERISTIC = UUID.from_16_bits(0x2BC3, 'Mute')
248
+
249
+ # Audio Stream Control Service (ASCS)
250
+ GATT_SINK_ASE_CHARACTERISTIC = UUID.from_16_bits(0x2BC4, 'Sink ASE')
251
+ GATT_SOURCE_ASE_CHARACTERISTIC = UUID.from_16_bits(0x2BC5, 'Source ASE')
252
+ GATT_ASE_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BC6, 'ASE Control Point')
253
+
254
+ # Broadcast Audio Scan Service (BASS)
255
+ GATT_BROADCAST_AUDIO_SCAN_CONTROL_POINT_CHARACTERISTIC = UUID.from_16_bits(0x2BC7, 'Broadcast Audio Scan Control Point')
256
+ GATT_BROADCAST_RECEIVE_STATE_CHARACTERISTIC = UUID.from_16_bits(0x2BC8, 'Broadcast Receive State')
257
+
258
+ # Published Audio Capabilities Service (PACS)
259
+ GATT_SINK_PAC_CHARACTERISTIC = UUID.from_16_bits(0x2BC9, 'Sink PAC')
260
+ GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC = UUID.from_16_bits(0x2BCA, 'Sink Audio Location')
261
+ GATT_SOURCE_PAC_CHARACTERISTIC = UUID.from_16_bits(0x2BCB, 'Source PAC')
262
+ GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC = UUID.from_16_bits(0x2BCC, 'Source Audio Location')
263
+ GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC = UUID.from_16_bits(0x2BCD, 'Available Audio Contexts')
264
+ GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC = UUID.from_16_bits(0x2BCE, 'Supported Audio Contexts')
265
+
159
266
  # ASHA Service
160
267
  GATT_ASHA_SERVICE = UUID.from_16_bits(0xFDF0, 'Audio Streaming for Hearing Aid')
161
268
  GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC = UUID('6333651e-c481-4a3e-9169-7c902aad37bb', 'ReadOnlyProperties')
@@ -177,6 +284,9 @@ GATT_BOOT_KEYBOARD_INPUT_REPORT_CHARACTERISTIC = UUID.from_16_bi
177
284
  GATT_CURRENT_TIME_CHARACTERISTIC = UUID.from_16_bits(0x2A2B, 'Current Time')
178
285
  GATT_BOOT_KEYBOARD_OUTPUT_REPORT_CHARACTERISTIC = UUID.from_16_bits(0x2A32, 'Boot Keyboard Output Report')
179
286
  GATT_CENTRAL_ADDRESS_RESOLUTION__CHARACTERISTIC = UUID.from_16_bits(0x2AA6, 'Central Address Resolution')
287
+ GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2B29, 'Client Supported Features')
288
+ GATT_DATABASE_HASH_CHARACTERISTIC = UUID.from_16_bits(0x2B2A, 'Database Hash')
289
+ GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2B3A, 'Server Supported Features')
180
290
 
181
291
  # fmt: on
182
292
  # pylint: enable=line-too-long
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/hci.py CHANGED
@@ -5296,6 +5296,10 @@ class HCI_Disconnection_Complete_Event(HCI_Event):
5296
5296
  See Bluetooth spec @ 7.7.5 Disconnection Complete Event
5297
5297
  '''
5298
5298
 
5299
+ status: int
5300
+ connection_handle: int
5301
+ reason: int
5302
+
5299
5303
 
5300
5304
  # -----------------------------------------------------------------------------
5301
5305
  @HCI_Event.event([('status', STATUS_SPEC), ('connection_handle', 2)])
bumble/helpers.py CHANGED
@@ -15,30 +15,39 @@
15
15
  # -----------------------------------------------------------------------------
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Callable, MutableMapping
21
+ from typing import cast, Any
18
22
  import logging
19
23
 
20
- from .colors import color
21
- from .att import ATT_CID, ATT_PDU
22
- from .smp import SMP_CID, SMP_Command
23
- from .core import name_or_number
24
- from .l2cap import (
24
+ from bumble import avdtp
25
+ from bumble.colors import color
26
+ from bumble.att import ATT_CID, ATT_PDU
27
+ from bumble.smp import SMP_CID, SMP_Command
28
+ from bumble.core import name_or_number
29
+ from bumble.l2cap import (
25
30
  L2CAP_PDU,
26
31
  L2CAP_CONNECTION_REQUEST,
27
32
  L2CAP_CONNECTION_RESPONSE,
28
33
  L2CAP_SIGNALING_CID,
29
34
  L2CAP_LE_SIGNALING_CID,
30
35
  L2CAP_Control_Frame,
36
+ L2CAP_Connection_Request,
31
37
  L2CAP_Connection_Response,
32
38
  )
33
- from .hci import (
39
+ from bumble.hci import (
34
40
  HCI_EVENT_PACKET,
35
41
  HCI_ACL_DATA_PACKET,
36
42
  HCI_DISCONNECTION_COMPLETE_EVENT,
37
43
  HCI_AclDataPacketAssembler,
44
+ HCI_Packet,
45
+ HCI_Event,
46
+ HCI_AclDataPacket,
47
+ HCI_Disconnection_Complete_Event,
38
48
  )
39
- from .rfcomm import RFCOMM_Frame, RFCOMM_PSM
40
- from .sdp import SDP_PDU, SDP_PSM
41
- from .avdtp import MessageAssembler as AVDTP_MessageAssembler, AVDTP_PSM
49
+ from bumble.rfcomm import RFCOMM_Frame, RFCOMM_PSM
50
+ from bumble.sdp import SDP_PDU, SDP_PSM
42
51
 
43
52
  # -----------------------------------------------------------------------------
44
53
  # Logging
@@ -50,23 +59,25 @@ logger = logging.getLogger(__name__)
50
59
  PSM_NAMES = {
51
60
  RFCOMM_PSM: 'RFCOMM',
52
61
  SDP_PSM: 'SDP',
53
- AVDTP_PSM: 'AVDTP'
54
- # TODO: add more PSM values
62
+ avdtp.AVDTP_PSM: 'AVDTP',
55
63
  }
56
64
 
57
65
 
58
66
  # -----------------------------------------------------------------------------
59
67
  class PacketTracer:
60
68
  class AclStream:
61
- def __init__(self, analyzer):
69
+ psms: MutableMapping[int, int]
70
+ peer: PacketTracer.AclStream
71
+ avdtp_assemblers: MutableMapping[int, avdtp.MessageAssembler]
72
+
73
+ def __init__(self, analyzer: PacketTracer.Analyzer) -> None:
62
74
  self.analyzer = analyzer
63
75
  self.packet_assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
64
76
  self.avdtp_assemblers = {} # AVDTP assemblers, by source_cid
65
77
  self.psms = {} # PSM, by source_cid
66
- self.peer = None # ACL stream in the other direction
67
78
 
68
79
  # pylint: disable=too-many-nested-blocks
69
- def on_acl_pdu(self, pdu):
80
+ def on_acl_pdu(self, pdu: bytes) -> None:
70
81
  l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
71
82
 
72
83
  if l2cap_pdu.cid == ATT_CID:
@@ -81,26 +92,30 @@ class PacketTracer:
81
92
 
82
93
  # Check if this signals a new channel
83
94
  if control_frame.code == L2CAP_CONNECTION_REQUEST:
84
- self.psms[control_frame.source_cid] = control_frame.psm
95
+ connection_request = cast(L2CAP_Connection_Request, control_frame)
96
+ self.psms[connection_request.source_cid] = connection_request.psm
85
97
  elif control_frame.code == L2CAP_CONNECTION_RESPONSE:
98
+ connection_response = cast(L2CAP_Connection_Response, control_frame)
86
99
  if (
87
- control_frame.result
100
+ connection_response.result
88
101
  == L2CAP_Connection_Response.CONNECTION_SUCCESSFUL
89
102
  ):
90
103
  if self.peer:
91
- if psm := self.peer.psms.get(control_frame.source_cid):
104
+ if psm := self.peer.psms.get(
105
+ connection_response.source_cid
106
+ ):
92
107
  # Found a pending connection
93
- self.psms[control_frame.destination_cid] = psm
108
+ self.psms[connection_response.destination_cid] = psm
94
109
 
95
110
  # For AVDTP connections, create a packet assembler for
96
111
  # each direction
97
- if psm == AVDTP_PSM:
112
+ if psm == avdtp.AVDTP_PSM:
98
113
  self.avdtp_assemblers[
99
- control_frame.source_cid
100
- ] = AVDTP_MessageAssembler(self.on_avdtp_message)
114
+ connection_response.source_cid
115
+ ] = avdtp.MessageAssembler(self.on_avdtp_message)
101
116
  self.peer.avdtp_assemblers[
102
- control_frame.destination_cid
103
- ] = AVDTP_MessageAssembler(
117
+ connection_response.destination_cid
118
+ ] = avdtp.MessageAssembler(
104
119
  self.peer.on_avdtp_message
105
120
  )
106
121
 
@@ -113,7 +128,7 @@ class PacketTracer:
113
128
  elif psm == RFCOMM_PSM:
114
129
  rfcomm_frame = RFCOMM_Frame.from_bytes(l2cap_pdu.payload)
115
130
  self.analyzer.emit(rfcomm_frame)
116
- elif psm == AVDTP_PSM:
131
+ elif psm == avdtp.AVDTP_PSM:
117
132
  self.analyzer.emit(
118
133
  f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, '
119
134
  f'PSM=AVDTP]: {l2cap_pdu.payload.hex()}'
@@ -130,22 +145,26 @@ class PacketTracer:
130
145
  else:
131
146
  self.analyzer.emit(l2cap_pdu)
132
147
 
133
- def on_avdtp_message(self, transaction_label, message):
148
+ def on_avdtp_message(
149
+ self, transaction_label: int, message: avdtp.Message
150
+ ) -> None:
134
151
  self.analyzer.emit(
135
152
  f'{color("AVDTP", "green")} [{transaction_label}] {message}'
136
153
  )
137
154
 
138
- def feed_packet(self, packet):
155
+ def feed_packet(self, packet: HCI_AclDataPacket) -> None:
139
156
  self.packet_assembler.feed_packet(packet)
140
157
 
141
158
  class Analyzer:
142
- def __init__(self, label, emit_message):
159
+ acl_streams: MutableMapping[int, PacketTracer.AclStream]
160
+ peer: PacketTracer.Analyzer
161
+
162
+ def __init__(self, label: str, emit_message: Callable[..., None]) -> None:
143
163
  self.label = label
144
164
  self.emit_message = emit_message
145
165
  self.acl_streams = {} # ACL streams, by connection handle
146
- self.peer = None # Analyzer in the other direction
147
166
 
148
- def start_acl_stream(self, connection_handle):
167
+ def start_acl_stream(self, connection_handle: int) -> PacketTracer.AclStream:
149
168
  logger.info(
150
169
  f'[{self.label}] +++ Creating ACL stream for connection '
151
170
  f'0x{connection_handle:04X}'
@@ -160,7 +179,7 @@ class PacketTracer:
160
179
 
161
180
  return stream
162
181
 
163
- def end_acl_stream(self, connection_handle):
182
+ def end_acl_stream(self, connection_handle: int) -> None:
164
183
  if connection_handle in self.acl_streams:
165
184
  logger.info(
166
185
  f'[{self.label}] --- Removing ACL stream for connection '
@@ -171,23 +190,29 @@ class PacketTracer:
171
190
  # Let the other forwarder know so it can cleanup its stream as well
172
191
  self.peer.end_acl_stream(connection_handle)
173
192
 
174
- def on_packet(self, packet):
193
+ def on_packet(self, packet: HCI_Packet) -> None:
175
194
  self.emit(packet)
176
195
 
177
196
  if packet.hci_packet_type == HCI_ACL_DATA_PACKET:
197
+ acl_packet = cast(HCI_AclDataPacket, packet)
178
198
  # Look for an existing stream for this handle, create one if it is the
179
199
  # first ACL packet for that connection handle
180
- if (stream := self.acl_streams.get(packet.connection_handle)) is None:
181
- stream = self.start_acl_stream(packet.connection_handle)
182
- stream.feed_packet(packet)
200
+ if (
201
+ stream := self.acl_streams.get(acl_packet.connection_handle)
202
+ ) is None:
203
+ stream = self.start_acl_stream(acl_packet.connection_handle)
204
+ stream.feed_packet(acl_packet)
183
205
  elif packet.hci_packet_type == HCI_EVENT_PACKET:
184
- if packet.event_code == HCI_DISCONNECTION_COMPLETE_EVENT:
185
- self.end_acl_stream(packet.connection_handle)
206
+ event_packet = cast(HCI_Event, packet)
207
+ if event_packet.event_code == HCI_DISCONNECTION_COMPLETE_EVENT:
208
+ self.end_acl_stream(
209
+ cast(HCI_Disconnection_Complete_Event, packet).connection_handle
210
+ )
186
211
 
187
- def emit(self, message):
212
+ def emit(self, message: Any) -> None:
188
213
  self.emit_message(f'[{self.label}] {message}')
189
214
 
190
- def trace(self, packet, direction=0):
215
+ def trace(self, packet: HCI_Packet, direction: int = 0) -> None:
191
216
  if direction == 0:
192
217
  self.host_to_controller_analyzer.on_packet(packet)
193
218
  else:
@@ -195,10 +220,10 @@ class PacketTracer:
195
220
 
196
221
  def __init__(
197
222
  self,
198
- host_to_controller_label=color('HOST->CONTROLLER', 'blue'),
199
- controller_to_host_label=color('CONTROLLER->HOST', 'cyan'),
200
- emit_message=logger.info,
201
- ):
223
+ host_to_controller_label: str = color('HOST->CONTROLLER', 'blue'),
224
+ controller_to_host_label: str = color('CONTROLLER->HOST', 'cyan'),
225
+ emit_message: Callable[..., None] = logger.info,
226
+ ) -> None:
202
227
  self.host_to_controller_analyzer = PacketTracer.Analyzer(
203
228
  host_to_controller_label, emit_message
204
229
  )
bumble/l2cap.py CHANGED
@@ -391,6 +391,9 @@ class L2CAP_Connection_Request(L2CAP_Control_Frame):
391
391
  See Bluetooth spec @ Vol 3, Part A - 4.2 CONNECTION REQUEST
392
392
  '''
393
393
 
394
+ psm: int
395
+ source_cid: int
396
+
394
397
  @staticmethod
395
398
  def parse_psm(data: bytes, offset: int = 0) -> Tuple[int, int]:
396
399
  psm_length = 2
@@ -432,6 +435,11 @@ class L2CAP_Connection_Response(L2CAP_Control_Frame):
432
435
  See Bluetooth spec @ Vol 3, Part A - 4.3 CONNECTION RESPONSE
433
436
  '''
434
437
 
438
+ source_cid: int
439
+ destination_cid: int
440
+ status: int
441
+ result: int
442
+
435
443
  CONNECTION_SUCCESSFUL = 0x0000
436
444
  CONNECTION_PENDING = 0x0001
437
445
  CONNECTION_REFUSED_PSM_NOT_SUPPORTED = 0x0002
@@ -0,0 +1,147 @@
1
+ # Copyright 2021-2023 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
+ # -----------------------------------------------------------------------------
17
+ # Imports
18
+ # -----------------------------------------------------------------------------
19
+ from __future__ import annotations
20
+ import enum
21
+ import struct
22
+ from typing import Optional
23
+
24
+ from bumble import gatt
25
+ from bumble import gatt_client
26
+
27
+
28
+ # -----------------------------------------------------------------------------
29
+ # Constants
30
+ # -----------------------------------------------------------------------------
31
+ class SirkType(enum.IntEnum):
32
+ '''Coordinated Set Identification Service - 5.1 Set Identity Resolving Key.'''
33
+
34
+ ENCRYPTED = 0x00
35
+ PLAINTEXT = 0x01
36
+
37
+
38
+ class MemberLock(enum.IntEnum):
39
+ '''Coordinated Set Identification Service - 5.3 Set Member Lock.'''
40
+
41
+ UNLOCKED = 0x01
42
+ LOCKED = 0x02
43
+
44
+
45
+ # -----------------------------------------------------------------------------
46
+ # Utils
47
+ # -----------------------------------------------------------------------------
48
+ # TODO: Implement RSI Generator
49
+
50
+
51
+ # -----------------------------------------------------------------------------
52
+ # Server
53
+ # -----------------------------------------------------------------------------
54
+ class CoordinatedSetIdentificationService(gatt.TemplateService):
55
+ UUID = gatt.GATT_COORDINATED_SET_IDENTIFICATION_SERVICE
56
+
57
+ set_identity_resolving_key_characteristic: gatt.Characteristic
58
+ coordinated_set_size_characteristic: Optional[gatt.Characteristic] = None
59
+ set_member_lock_characteristic: Optional[gatt.Characteristic] = None
60
+ set_member_rank_characteristic: Optional[gatt.Characteristic] = None
61
+
62
+ def __init__(
63
+ self,
64
+ set_identity_resolving_key: bytes,
65
+ coordinated_set_size: Optional[int] = None,
66
+ set_member_lock: Optional[MemberLock] = None,
67
+ set_member_rank: Optional[int] = None,
68
+ ) -> None:
69
+ characteristics = []
70
+
71
+ self.set_identity_resolving_key_characteristic = gatt.Characteristic(
72
+ uuid=gatt.GATT_SET_IDENTITY_RESOLVING_KEY_CHARACTERISTIC,
73
+ properties=gatt.Characteristic.Properties.READ
74
+ | gatt.Characteristic.Properties.NOTIFY,
75
+ permissions=gatt.Characteristic.Permissions.READABLE,
76
+ # TODO: Implement encrypted SIRK reader.
77
+ value=struct.pack('B', SirkType.PLAINTEXT) + set_identity_resolving_key,
78
+ )
79
+ characteristics.append(self.set_identity_resolving_key_characteristic)
80
+
81
+ if coordinated_set_size is not None:
82
+ self.coordinated_set_size_characteristic = gatt.Characteristic(
83
+ uuid=gatt.GATT_COORDINATED_SET_SIZE_CHARACTERISTIC,
84
+ properties=gatt.Characteristic.Properties.READ
85
+ | gatt.Characteristic.Properties.NOTIFY,
86
+ permissions=gatt.Characteristic.Permissions.READABLE,
87
+ value=struct.pack('B', coordinated_set_size),
88
+ )
89
+ characteristics.append(self.coordinated_set_size_characteristic)
90
+
91
+ if set_member_lock is not None:
92
+ self.set_member_lock_characteristic = gatt.Characteristic(
93
+ uuid=gatt.GATT_SET_MEMBER_LOCK_CHARACTERISTIC,
94
+ properties=gatt.Characteristic.Properties.READ
95
+ | gatt.Characteristic.Properties.NOTIFY
96
+ | gatt.Characteristic.Properties.WRITE,
97
+ permissions=gatt.Characteristic.Permissions.READABLE
98
+ | gatt.Characteristic.Permissions.WRITEABLE,
99
+ value=struct.pack('B', set_member_lock),
100
+ )
101
+ characteristics.append(self.set_member_lock_characteristic)
102
+
103
+ if set_member_rank is not None:
104
+ self.set_member_rank_characteristic = gatt.Characteristic(
105
+ uuid=gatt.GATT_SET_MEMBER_RANK_CHARACTERISTIC,
106
+ properties=gatt.Characteristic.Properties.READ
107
+ | gatt.Characteristic.Properties.NOTIFY,
108
+ permissions=gatt.Characteristic.Permissions.READABLE,
109
+ value=struct.pack('B', set_member_rank),
110
+ )
111
+ characteristics.append(self.set_member_rank_characteristic)
112
+
113
+ super().__init__(characteristics)
114
+
115
+
116
+ # -----------------------------------------------------------------------------
117
+ # Client
118
+ # -----------------------------------------------------------------------------
119
+ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
120
+ SERVICE_CLASS = CoordinatedSetIdentificationService
121
+
122
+ set_identity_resolving_key: gatt_client.CharacteristicProxy
123
+ coordinated_set_size: Optional[gatt_client.CharacteristicProxy] = None
124
+ set_member_lock: Optional[gatt_client.CharacteristicProxy] = None
125
+ set_member_rank: Optional[gatt_client.CharacteristicProxy] = None
126
+
127
+ def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
128
+ self.service_proxy = service_proxy
129
+
130
+ self.set_identity_resolving_key = service_proxy.get_characteristics_by_uuid(
131
+ gatt.GATT_SET_IDENTITY_RESOLVING_KEY_CHARACTERISTIC
132
+ )[0]
133
+
134
+ if characteristics := service_proxy.get_characteristics_by_uuid(
135
+ gatt.GATT_COORDINATED_SET_SIZE_CHARACTERISTIC
136
+ ):
137
+ self.coordinated_set_size = characteristics[0]
138
+
139
+ if characteristics := service_proxy.get_characteristics_by_uuid(
140
+ gatt.GATT_SET_MEMBER_LOCK_CHARACTERISTIC
141
+ ):
142
+ self.set_member_lock = characteristics[0]
143
+
144
+ if characteristics := service_proxy.get_characteristics_by_uuid(
145
+ gatt.GATT_SET_MEMBER_RANK_CHARACTERISTIC
146
+ ):
147
+ self.set_member_rank = characteristics[0]
@@ -150,7 +150,7 @@ class PacketParser:
150
150
  try:
151
151
  self.sink.on_packet(bytes(self.packet))
152
152
  except Exception as error:
153
- logger.warning(
153
+ logger.exception(
154
154
  color(f'!!! Exception in on_packet: {error}', 'red')
155
155
  )
156
156
  self.reset()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bumble
3
- Version: 0.0.179
3
+ Version: 0.0.180
4
4
  Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
5
5
  Home-page: https://github.com/google/bumble
6
6
  Author: Google
@@ -1,5 +1,5 @@
1
1
  bumble/__init__.py,sha256=Q8jkz6rgl95IMAeInQVt_2GLoJl3DcEP2cxtrQ-ho5c,110
2
- bumble/_version.py,sha256=C1R6E3aAR9_nS3B_vWCmuIcNp4WVyDZy_cpCgeJmaUg,415
2
+ bumble/_version.py,sha256=5oaj9BgfeJteuh4NknBm42CZG1RsQs3FBEs6JGSoHXE,415
3
3
  bumble/a2dp.py,sha256=NqdmHIeEe3kThoHqdOgC3Wkywc6H7zqwGbjRYYFtNoQ,22321
4
4
  bumble/at.py,sha256=kdrcsx2C8Rg61EWESD2QHwpZntkXkRBJLrPn9auv9K8,2961
5
5
  bumble/att.py,sha256=emTsIoc_C067QE3osdyocic52Wr8N5TJdkpZeJJH6dk,31328
@@ -12,18 +12,18 @@ bumble/controller.py,sha256=9lyDMD836oK5nqvwfjFEQgQ3bTl56qn4kT5ix-HJc8w,45204
12
12
  bumble/core.py,sha256=HjRpUseyVsueNkZ-KhPPTf1vNkNnI02eNcaOe685V_0,52949
13
13
  bumble/crypto.py,sha256=o3lzVPeiaPX_aNeeg2Ir-UHiGpNU7Z2VyimKzDwMsHw,9012
14
14
  bumble/decoder.py,sha256=N9nMvuVhuwpnfw7EDVuNe9uYY6B6c3RY2dh8RhRPC1U,9608
15
- bumble/device.py,sha256=h8JnJva0jGxd-_Cm3X235pLmppMq3zDsp6FfqbVvKYQ,135471
15
+ bumble/device.py,sha256=JMRrTiK6ua9tpNRYL_491NpwC0mjBsd3TDBUVzj7Ms4,136996
16
16
  bumble/gap.py,sha256=axlOZIv99357Ehq2vMokeioU85z81qdQvplGn0pF70Q,2137
17
- bumble/gatt.py,sha256=PtyDIsswJrp8g96E_Hrp7HzvLe9DEv2YBO6yIEDQ5sg,28625
18
- bumble/gatt_client.py,sha256=c7QzKx2sxJv7ZHq1cDsJSnnQpTPnuX9OZ-PNpmcwEKE,41493
17
+ bumble/gatt.py,sha256=ASbYauOKyOHgHrN67Ue12MFU1rayqePG5mEI0vaS0SY,38191
18
+ bumble/gatt_client.py,sha256=JuyFUHEc5xd2astr2XCZrTiAla2FoDkKe4lH1oSQOPA,42551
19
19
  bumble/gatt_server.py,sha256=pLkydlyqQTyi9g89-WH7tZTIVtOd8gi6bpUn2b-OzAU,36728
20
- bumble/hci.py,sha256=TqdewoypYGQLoD5DBVI6YlypEzWNaAKAKqyNuJtJE4o,231689
21
- bumble/helpers.py,sha256=AgtOigAqQzzTEMMwHQJ2EO7dOTp77qhnyfHQ2LCDd-k,8896
20
+ bumble/hci.py,sha256=Kbu2oESm6Xa3AkdyVzbU64OLzAzHXiQfLTO1tfGfoGo,231749
21
+ bumble/helpers.py,sha256=M7yGMbyI6_ZxIUNZA2krWJXP1qkedVcCOYog1lNPm-M,10084
22
22
  bumble/hfp.py,sha256=SzoYpcXgY3LEHVEKUT64PFAJ7G5TZ29hhBbNnW9v8Cs,39067
23
23
  bumble/hid.py,sha256=O_FzevrBsCVzuCL5v28NY5VoXmTNQ_4ditW3MYuOaP8,11341
24
24
  bumble/host.py,sha256=woncM0_1siA9WKIAVkKjut3dUKiiDJbVzkTj3V3WKIw,36999
25
25
  bumble/keys.py,sha256=_ibaJ4CxM5zyxnTp7wnihIQSsEAEkCiPxx9qHsGA04Q,12601
26
- bumble/l2cap.py,sha256=nq3llrPHgeeFGvHVqaLdR6gcYgkRq6vD81BsVOwCQ3Y,80666
26
+ bumble/l2cap.py,sha256=1wtMmpVMAY5Djfo7Rs8pKKU6ulBqutCY7UGvv8Vnfck,80778
27
27
  bumble/link.py,sha256=dvb3fdmV8689A4rLhFVHHKMR3u4wQbUapFi-GeheK0M,20220
28
28
  bumble/pairing.py,sha256=tgPUba6xNxMi-2plm3xfRlzHq-uPRNZEIGWaN0qNGCs,9853
29
29
  bumble/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -69,6 +69,7 @@ bumble/pandora/utils.py,sha256=Fq4glL0T5cJ2FODoDotmDNdYFOkTOR7DyyL8vkcxp20,3949
69
69
  bumble/profiles/__init__.py,sha256=yBGC8Ti5LvZuoh1F42XtfrBilb39T77_yuxESZeX2yI,581
70
70
  bumble/profiles/asha_service.py,sha256=wKAYgc36a9mBWx9wfIofA2e1fpnErrjHM-U1GfCX9LQ,7200
71
71
  bumble/profiles/battery_service.py,sha256=w-uF4jLoDozJOoykimb2RkrKjVyCke6ts2-h-F1PYyc,2292
72
+ bumble/profiles/csip.py,sha256=PQ8A_5pJa2R96g4_wKM4JGgEFuvK7SAjoXS1xDLXt4g,6144
72
73
  bumble/profiles/device_information_service.py,sha256=H1Db4BAOnsC-rRtfpoAIsDETYT4F9yM_WgByn_3LfRQ,5658
73
74
  bumble/profiles/heart_rate_service.py,sha256=7V2LGcWLp6RurjWxsVgMWr3wPDt5aS9qjNxTbHcOK6o,8575
74
75
  bumble/profiles/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -79,7 +80,7 @@ bumble/tools/rtk_util.py,sha256=TwZhupHQrQYsYHLdRGyzXKd24pwCk8kkzqK1Rj2guco,5087
79
80
  bumble/transport/__init__.py,sha256=Yz2aau52Hi0B6sh06WtfJzMwOYr1Y5VlqjkJCw4hcbA,5901
80
81
  bumble/transport/android_emulator.py,sha256=n6nPti0eb6JqPkAj5-fdtiMfSzA2Hgd2q4B1arudIhM,4333
81
82
  bumble/transport/android_netsim.py,sha256=SVh-IUZ2bhcIESZFGzOsofybsi4H0qoBRwBieeqUINE,16215
82
- bumble/transport/common.py,sha256=WRAj8VPUGHx_xys1-vLfVXRcTO84pSSbRYwBdBERg1Q,15605
83
+ bumble/transport/common.py,sha256=pOnr1GdIQtz8BM7-O3Q4OlEbMs5tfU3Fc9zFo2b62S0,15607
83
84
  bumble/transport/file.py,sha256=eVM2V6Nk2nDAFdE7Rt01ZI3JdTovsH9OEU1gKYPJjpE,2010
84
85
  bumble/transport/hci_socket.py,sha256=y9hrIY7QIgP994lffJHaAi2jfpC9FCANhzHO5F6k3vk,6377
85
86
  bumble/transport/pty.py,sha256=grTl-yvjMWHflNwuME4ccVqDbk6NIEgQMgH6Y9lf1fU,2732
@@ -126,9 +127,9 @@ bumble/vendor/android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
126
127
  bumble/vendor/android/hci.py,sha256=GZrkhaWmcMt1JpnRhv0NoySGkf2H4lNUV2f_omRZW0I,10741
127
128
  bumble/vendor/zephyr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
129
  bumble/vendor/zephyr/hci.py,sha256=d83bC0TvT947eN4roFjLkQefWtHOoNsr4xib2ctSkvA,3195
129
- bumble-0.0.179.dist-info/LICENSE,sha256=FvaYh4NRWIGgS_OwoBs5gFgkCmAghZ-DYnIGBZPuw-s,12142
130
- bumble-0.0.179.dist-info/METADATA,sha256=AI5J0MtE_M5bWveSY4Cb58foHat9_8nDdDSRQ7GkxwA,5681
131
- bumble-0.0.179.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
132
- bumble-0.0.179.dist-info/entry_points.txt,sha256=AjCwgm9SvZDOhV7T6jWwAhWdE728pd759LQCscMLjnM,765
133
- bumble-0.0.179.dist-info/top_level.txt,sha256=tV6JJKaHPYMFiJYiBYFW24PCcfLxTJZdlu6BmH3Cb00,7
134
- bumble-0.0.179.dist-info/RECORD,,
130
+ bumble-0.0.180.dist-info/LICENSE,sha256=FvaYh4NRWIGgS_OwoBs5gFgkCmAghZ-DYnIGBZPuw-s,12142
131
+ bumble-0.0.180.dist-info/METADATA,sha256=U4ohfLC-oVuJa-g-Dllxl0QaucDvft6qL2o7O43T3Sw,5681
132
+ bumble-0.0.180.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
133
+ bumble-0.0.180.dist-info/entry_points.txt,sha256=AjCwgm9SvZDOhV7T6jWwAhWdE728pd759LQCscMLjnM,765
134
+ bumble-0.0.180.dist-info/top_level.txt,sha256=tV6JJKaHPYMFiJYiBYFW24PCcfLxTJZdlu6BmH3Cb00,7
135
+ bumble-0.0.180.dist-info/RECORD,,