bumble 0.0.195__py3-none-any.whl → 0.0.199__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.
Files changed (61) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/auracast.py +351 -66
  3. bumble/apps/console.py +5 -20
  4. bumble/apps/device_info.py +230 -0
  5. bumble/apps/gatt_dump.py +4 -0
  6. bumble/apps/lea_unicast/app.py +16 -17
  7. bumble/apps/pair.py +32 -5
  8. bumble/at.py +12 -6
  9. bumble/att.py +56 -40
  10. bumble/avc.py +8 -5
  11. bumble/avctp.py +3 -2
  12. bumble/avdtp.py +7 -3
  13. bumble/avrcp.py +2 -1
  14. bumble/codecs.py +17 -13
  15. bumble/colors.py +6 -2
  16. bumble/core.py +37 -7
  17. bumble/decoder.py +14 -10
  18. bumble/device.py +382 -111
  19. bumble/drivers/rtk.py +32 -13
  20. bumble/gatt.py +30 -20
  21. bumble/gatt_client.py +15 -29
  22. bumble/gatt_server.py +14 -6
  23. bumble/hci.py +322 -32
  24. bumble/hid.py +24 -28
  25. bumble/host.py +20 -6
  26. bumble/l2cap.py +24 -17
  27. bumble/link.py +8 -3
  28. bumble/pandora/__init__.py +3 -0
  29. bumble/pandora/l2cap.py +310 -0
  30. bumble/profiles/aics.py +520 -0
  31. bumble/profiles/ascs.py +739 -0
  32. bumble/profiles/asha.py +295 -0
  33. bumble/profiles/bap.py +1 -874
  34. bumble/profiles/bass.py +440 -0
  35. bumble/profiles/csip.py +4 -4
  36. bumble/profiles/gap.py +110 -0
  37. bumble/profiles/hap.py +665 -0
  38. bumble/profiles/heart_rate_service.py +4 -3
  39. bumble/profiles/le_audio.py +43 -9
  40. bumble/profiles/mcp.py +448 -0
  41. bumble/profiles/pacs.py +210 -0
  42. bumble/profiles/tmap.py +89 -0
  43. bumble/profiles/vcp.py +5 -3
  44. bumble/rfcomm.py +4 -2
  45. bumble/sdp.py +13 -11
  46. bumble/smp.py +43 -12
  47. bumble/snoop.py +5 -4
  48. bumble/transport/__init__.py +8 -2
  49. bumble/transport/android_emulator.py +9 -3
  50. bumble/transport/android_netsim.py +9 -7
  51. bumble/transport/common.py +46 -18
  52. bumble/transport/pyusb.py +21 -4
  53. bumble/transport/unix.py +56 -0
  54. bumble/transport/usb.py +57 -46
  55. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/METADATA +41 -41
  56. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/RECORD +60 -49
  57. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/WHEEL +1 -1
  58. bumble/profiles/asha_service.py +0 -193
  59. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/LICENSE +0 -0
  60. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/entry_points.txt +0 -0
  61. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,210 @@
1
+ # Copyright 2024 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
13
+
14
+ """LE Audio - Published Audio Capabilities Service"""
15
+
16
+ # -----------------------------------------------------------------------------
17
+ # Imports
18
+ # -----------------------------------------------------------------------------
19
+ from __future__ import annotations
20
+ import dataclasses
21
+ import logging
22
+ import struct
23
+ from typing import Optional, Sequence, Union
24
+
25
+ from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType
26
+ from bumble.profiles import le_audio
27
+ from bumble import gatt
28
+ from bumble import gatt_client
29
+ from bumble import hci
30
+
31
+
32
+ # -----------------------------------------------------------------------------
33
+ # Logging
34
+ # -----------------------------------------------------------------------------
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ # -----------------------------------------------------------------------------
39
+ @dataclasses.dataclass
40
+ class PacRecord:
41
+ '''Published Audio Capabilities Service, Table 3.2/3.4.'''
42
+
43
+ coding_format: hci.CodingFormat
44
+ codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
45
+ metadata: le_audio.Metadata = dataclasses.field(default_factory=le_audio.Metadata)
46
+
47
+ @classmethod
48
+ def from_bytes(cls, data: bytes) -> PacRecord:
49
+ offset, coding_format = hci.CodingFormat.parse_from_bytes(data, 0)
50
+ codec_specific_capabilities_size = data[offset]
51
+
52
+ offset += 1
53
+ codec_specific_capabilities_bytes = data[
54
+ offset : offset + codec_specific_capabilities_size
55
+ ]
56
+ offset += codec_specific_capabilities_size
57
+ metadata_size = data[offset]
58
+ offset += 1
59
+ metadata = le_audio.Metadata.from_bytes(data[offset : offset + metadata_size])
60
+
61
+ codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
62
+ if coding_format.codec_id == hci.CodecID.VENDOR_SPECIFIC:
63
+ codec_specific_capabilities = codec_specific_capabilities_bytes
64
+ else:
65
+ codec_specific_capabilities = CodecSpecificCapabilities.from_bytes(
66
+ codec_specific_capabilities_bytes
67
+ )
68
+
69
+ return PacRecord(
70
+ coding_format=coding_format,
71
+ codec_specific_capabilities=codec_specific_capabilities,
72
+ metadata=metadata,
73
+ )
74
+
75
+ def __bytes__(self) -> bytes:
76
+ capabilities_bytes = bytes(self.codec_specific_capabilities)
77
+ metadata_bytes = bytes(self.metadata)
78
+ return (
79
+ bytes(self.coding_format)
80
+ + bytes([len(capabilities_bytes)])
81
+ + capabilities_bytes
82
+ + bytes([len(metadata_bytes)])
83
+ + metadata_bytes
84
+ )
85
+
86
+
87
+ # -----------------------------------------------------------------------------
88
+ # Server
89
+ # -----------------------------------------------------------------------------
90
+ class PublishedAudioCapabilitiesService(gatt.TemplateService):
91
+ UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
92
+
93
+ sink_pac: Optional[gatt.Characteristic]
94
+ sink_audio_locations: Optional[gatt.Characteristic]
95
+ source_pac: Optional[gatt.Characteristic]
96
+ source_audio_locations: Optional[gatt.Characteristic]
97
+ available_audio_contexts: gatt.Characteristic
98
+ supported_audio_contexts: gatt.Characteristic
99
+
100
+ def __init__(
101
+ self,
102
+ supported_source_context: ContextType,
103
+ supported_sink_context: ContextType,
104
+ available_source_context: ContextType,
105
+ available_sink_context: ContextType,
106
+ sink_pac: Sequence[PacRecord] = (),
107
+ sink_audio_locations: Optional[AudioLocation] = None,
108
+ source_pac: Sequence[PacRecord] = (),
109
+ source_audio_locations: Optional[AudioLocation] = None,
110
+ ) -> None:
111
+ characteristics = []
112
+
113
+ self.supported_audio_contexts = gatt.Characteristic(
114
+ uuid=gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC,
115
+ properties=gatt.Characteristic.Properties.READ,
116
+ permissions=gatt.Characteristic.Permissions.READABLE,
117
+ value=struct.pack('<HH', supported_sink_context, supported_source_context),
118
+ )
119
+ characteristics.append(self.supported_audio_contexts)
120
+
121
+ self.available_audio_contexts = gatt.Characteristic(
122
+ uuid=gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC,
123
+ properties=gatt.Characteristic.Properties.READ
124
+ | gatt.Characteristic.Properties.NOTIFY,
125
+ permissions=gatt.Characteristic.Permissions.READABLE,
126
+ value=struct.pack('<HH', available_sink_context, available_source_context),
127
+ )
128
+ characteristics.append(self.available_audio_contexts)
129
+
130
+ if sink_pac:
131
+ self.sink_pac = gatt.Characteristic(
132
+ uuid=gatt.GATT_SINK_PAC_CHARACTERISTIC,
133
+ properties=gatt.Characteristic.Properties.READ,
134
+ permissions=gatt.Characteristic.Permissions.READABLE,
135
+ value=bytes([len(sink_pac)]) + b''.join(map(bytes, sink_pac)),
136
+ )
137
+ characteristics.append(self.sink_pac)
138
+
139
+ if sink_audio_locations is not None:
140
+ self.sink_audio_locations = gatt.Characteristic(
141
+ uuid=gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC,
142
+ properties=gatt.Characteristic.Properties.READ,
143
+ permissions=gatt.Characteristic.Permissions.READABLE,
144
+ value=struct.pack('<I', sink_audio_locations),
145
+ )
146
+ characteristics.append(self.sink_audio_locations)
147
+
148
+ if source_pac:
149
+ self.source_pac = gatt.Characteristic(
150
+ uuid=gatt.GATT_SOURCE_PAC_CHARACTERISTIC,
151
+ properties=gatt.Characteristic.Properties.READ,
152
+ permissions=gatt.Characteristic.Permissions.READABLE,
153
+ value=bytes([len(source_pac)]) + b''.join(map(bytes, source_pac)),
154
+ )
155
+ characteristics.append(self.source_pac)
156
+
157
+ if source_audio_locations is not None:
158
+ self.source_audio_locations = gatt.Characteristic(
159
+ uuid=gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC,
160
+ properties=gatt.Characteristic.Properties.READ,
161
+ permissions=gatt.Characteristic.Permissions.READABLE,
162
+ value=struct.pack('<I', source_audio_locations),
163
+ )
164
+ characteristics.append(self.source_audio_locations)
165
+
166
+ super().__init__(characteristics)
167
+
168
+
169
+ # -----------------------------------------------------------------------------
170
+ # Client
171
+ # -----------------------------------------------------------------------------
172
+ class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
173
+ SERVICE_CLASS = PublishedAudioCapabilitiesService
174
+
175
+ sink_pac: Optional[gatt_client.CharacteristicProxy] = None
176
+ sink_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
177
+ source_pac: Optional[gatt_client.CharacteristicProxy] = None
178
+ source_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
179
+ available_audio_contexts: gatt_client.CharacteristicProxy
180
+ supported_audio_contexts: gatt_client.CharacteristicProxy
181
+
182
+ def __init__(self, service_proxy: gatt_client.ServiceProxy):
183
+ self.service_proxy = service_proxy
184
+
185
+ self.available_audio_contexts = service_proxy.get_characteristics_by_uuid(
186
+ gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
187
+ )[0]
188
+ self.supported_audio_contexts = service_proxy.get_characteristics_by_uuid(
189
+ gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
190
+ )[0]
191
+
192
+ if characteristics := service_proxy.get_characteristics_by_uuid(
193
+ gatt.GATT_SINK_PAC_CHARACTERISTIC
194
+ ):
195
+ self.sink_pac = characteristics[0]
196
+
197
+ if characteristics := service_proxy.get_characteristics_by_uuid(
198
+ gatt.GATT_SOURCE_PAC_CHARACTERISTIC
199
+ ):
200
+ self.source_pac = characteristics[0]
201
+
202
+ if characteristics := service_proxy.get_characteristics_by_uuid(
203
+ gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC
204
+ ):
205
+ self.sink_audio_locations = characteristics[0]
206
+
207
+ if characteristics := service_proxy.get_characteristics_by_uuid(
208
+ gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC
209
+ ):
210
+ self.source_audio_locations = characteristics[0]
@@ -0,0 +1,89 @@
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
+ """LE Audio - Telephony and Media Audio Profile"""
16
+
17
+ # -----------------------------------------------------------------------------
18
+ # Imports
19
+ # -----------------------------------------------------------------------------
20
+ import enum
21
+ import logging
22
+ import struct
23
+
24
+ from bumble.gatt import (
25
+ TemplateService,
26
+ Characteristic,
27
+ DelegatedCharacteristicAdapter,
28
+ InvalidServiceError,
29
+ GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE,
30
+ GATT_TMAP_ROLE_CHARACTERISTIC,
31
+ )
32
+ from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
33
+
34
+
35
+ # -----------------------------------------------------------------------------
36
+ # Logging
37
+ # -----------------------------------------------------------------------------
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ # -----------------------------------------------------------------------------
42
+ # Classes
43
+ # -----------------------------------------------------------------------------
44
+ class Role(enum.IntFlag):
45
+ CALL_GATEWAY = 1 << 0
46
+ CALL_TERMINAL = 1 << 1
47
+ UNICAST_MEDIA_SENDER = 1 << 2
48
+ UNICAST_MEDIA_RECEIVER = 1 << 3
49
+ BROADCAST_MEDIA_SENDER = 1 << 4
50
+ BROADCAST_MEDIA_RECEIVER = 1 << 5
51
+
52
+
53
+ # -----------------------------------------------------------------------------
54
+ class TelephonyAndMediaAudioService(TemplateService):
55
+ UUID = GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE
56
+
57
+ def __init__(self, role: Role):
58
+ self.role_characteristic = Characteristic(
59
+ GATT_TMAP_ROLE_CHARACTERISTIC,
60
+ Characteristic.Properties.READ,
61
+ Characteristic.READABLE,
62
+ struct.pack('<H', int(role)),
63
+ )
64
+
65
+ super().__init__([self.role_characteristic])
66
+
67
+
68
+ # -----------------------------------------------------------------------------
69
+ class TelephonyAndMediaAudioServiceProxy(ProfileServiceProxy):
70
+ SERVICE_CLASS = TelephonyAndMediaAudioService
71
+
72
+ role: DelegatedCharacteristicAdapter
73
+
74
+ def __init__(self, service_proxy: ServiceProxy):
75
+ self.service_proxy = service_proxy
76
+
77
+ if not (
78
+ characteristics := service_proxy.get_characteristics_by_uuid(
79
+ GATT_TMAP_ROLE_CHARACTERISTIC
80
+ )
81
+ ):
82
+ raise InvalidServiceError('TMAP Role characteristic not found')
83
+
84
+ self.role = DelegatedCharacteristicAdapter(
85
+ characteristics[0],
86
+ decode=lambda value: Role(
87
+ struct.unpack_from('<H', value, 0)[0],
88
+ ),
89
+ )
bumble/profiles/vcp.py CHANGED
@@ -24,7 +24,7 @@ from bumble import device
24
24
  from bumble import gatt
25
25
  from bumble import gatt_client
26
26
 
27
- from typing import Optional
27
+ from typing import Optional, Sequence
28
28
 
29
29
  # -----------------------------------------------------------------------------
30
30
  # Constants
@@ -88,6 +88,7 @@ class VolumeControlService(gatt.TemplateService):
88
88
  muted: int = 0,
89
89
  change_counter: int = 0,
90
90
  volume_flags: int = 0,
91
+ included_services: Sequence[gatt.Service] = (),
91
92
  ) -> None:
92
93
  self.step_size = step_size
93
94
  self.volume_setting = volume_setting
@@ -117,11 +118,12 @@ class VolumeControlService(gatt.TemplateService):
117
118
  )
118
119
 
119
120
  super().__init__(
120
- [
121
+ characteristics=[
121
122
  self.volume_state,
122
123
  self.volume_control_point,
123
124
  self.volume_flags,
124
- ]
125
+ ],
126
+ included_services=list(included_services),
125
127
  )
126
128
 
127
129
  @property
bumble/rfcomm.py CHANGED
@@ -36,7 +36,9 @@ from .core import (
36
36
  BT_RFCOMM_PROTOCOL_ID,
37
37
  BT_BR_EDR_TRANSPORT,
38
38
  BT_L2CAP_PROTOCOL_ID,
39
+ InvalidArgumentError,
39
40
  InvalidStateError,
41
+ InvalidPacketError,
40
42
  ProtocolError,
41
43
  )
42
44
 
@@ -335,7 +337,7 @@ class RFCOMM_Frame:
335
337
  frame = RFCOMM_Frame(frame_type, c_r, dlci, p_f, information)
336
338
  if frame.fcs != fcs:
337
339
  logger.warning(f'FCS mismatch: got {fcs:02X}, expected {frame.fcs:02X}')
338
- raise ValueError('fcs mismatch')
340
+ raise InvalidPacketError('fcs mismatch')
339
341
 
340
342
  return frame
341
343
 
@@ -713,7 +715,7 @@ class DLC(EventEmitter):
713
715
  # Automatically convert strings to bytes using UTF-8
714
716
  data = data.encode('utf-8')
715
717
  else:
716
- raise ValueError('write only accept bytes or strings')
718
+ raise InvalidArgumentError('write only accept bytes or strings')
717
719
 
718
720
  self.tx_buffer += data
719
721
  self.drained.clear()
bumble/sdp.py CHANGED
@@ -23,7 +23,7 @@ from typing_extensions import Self
23
23
 
24
24
  from . import core, l2cap
25
25
  from .colors import color
26
- from .core import InvalidStateError
26
+ from .core import InvalidStateError, InvalidArgumentError, InvalidPacketError
27
27
  from .hci import HCI_Object, name_or_number, key_with_value
28
28
 
29
29
  if TYPE_CHECKING:
@@ -189,7 +189,9 @@ class DataElement:
189
189
  self.bytes = None
190
190
  if element_type in (DataElement.UNSIGNED_INTEGER, DataElement.SIGNED_INTEGER):
191
191
  if value_size is None:
192
- raise ValueError('integer types must have a value size specified')
192
+ raise InvalidArgumentError(
193
+ 'integer types must have a value size specified'
194
+ )
193
195
 
194
196
  @staticmethod
195
197
  def nil() -> DataElement:
@@ -265,7 +267,7 @@ class DataElement:
265
267
  if len(data) == 8:
266
268
  return struct.unpack('>Q', data)[0]
267
269
 
268
- raise ValueError(f'invalid integer length {len(data)}')
270
+ raise InvalidPacketError(f'invalid integer length {len(data)}')
269
271
 
270
272
  @staticmethod
271
273
  def signed_integer_from_bytes(data):
@@ -281,7 +283,7 @@ class DataElement:
281
283
  if len(data) == 8:
282
284
  return struct.unpack('>q', data)[0]
283
285
 
284
- raise ValueError(f'invalid integer length {len(data)}')
286
+ raise InvalidPacketError(f'invalid integer length {len(data)}')
285
287
 
286
288
  @staticmethod
287
289
  def list_from_bytes(data):
@@ -354,7 +356,7 @@ class DataElement:
354
356
  data = b''
355
357
  elif self.type == DataElement.UNSIGNED_INTEGER:
356
358
  if self.value < 0:
357
- raise ValueError('UNSIGNED_INTEGER cannot be negative')
359
+ raise InvalidArgumentError('UNSIGNED_INTEGER cannot be negative')
358
360
 
359
361
  if self.value_size == 1:
360
362
  data = struct.pack('B', self.value)
@@ -365,7 +367,7 @@ class DataElement:
365
367
  elif self.value_size == 8:
366
368
  data = struct.pack('>Q', self.value)
367
369
  else:
368
- raise ValueError('invalid value_size')
370
+ raise InvalidArgumentError('invalid value_size')
369
371
  elif self.type == DataElement.SIGNED_INTEGER:
370
372
  if self.value_size == 1:
371
373
  data = struct.pack('b', self.value)
@@ -376,7 +378,7 @@ class DataElement:
376
378
  elif self.value_size == 8:
377
379
  data = struct.pack('>q', self.value)
378
380
  else:
379
- raise ValueError('invalid value_size')
381
+ raise InvalidArgumentError('invalid value_size')
380
382
  elif self.type == DataElement.UUID:
381
383
  data = bytes(reversed(bytes(self.value)))
382
384
  elif self.type == DataElement.URL:
@@ -392,7 +394,7 @@ class DataElement:
392
394
  size_bytes = b''
393
395
  if self.type == DataElement.NIL:
394
396
  if size != 0:
395
- raise ValueError('NIL must be empty')
397
+ raise InvalidArgumentError('NIL must be empty')
396
398
  size_index = 0
397
399
  elif self.type in (
398
400
  DataElement.UNSIGNED_INTEGER,
@@ -410,7 +412,7 @@ class DataElement:
410
412
  elif size == 16:
411
413
  size_index = 4
412
414
  else:
413
- raise ValueError('invalid data size')
415
+ raise InvalidArgumentError('invalid data size')
414
416
  elif self.type in (
415
417
  DataElement.TEXT_STRING,
416
418
  DataElement.SEQUENCE,
@@ -427,10 +429,10 @@ class DataElement:
427
429
  size_index = 7
428
430
  size_bytes = struct.pack('>I', size)
429
431
  else:
430
- raise ValueError('invalid data size')
432
+ raise InvalidArgumentError('invalid data size')
431
433
  elif self.type == DataElement.BOOLEAN:
432
434
  if size != 1:
433
- raise ValueError('boolean must be 1 byte')
435
+ raise InvalidArgumentError('boolean must be 1 byte')
434
436
  size_index = 0
435
437
 
436
438
  self.bytes = bytes([self.type << 3 | size_index]) + size_bytes + data
bumble/smp.py CHANGED
@@ -55,6 +55,7 @@ from .core import (
55
55
  BT_CENTRAL_ROLE,
56
56
  BT_LE_TRANSPORT,
57
57
  AdvertisingData,
58
+ InvalidArgumentError,
58
59
  ProtocolError,
59
60
  name_or_number,
60
61
  )
@@ -763,11 +764,16 @@ class Session:
763
764
  self.peer_io_capability = SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
764
765
 
765
766
  # OOB
766
- self.oob_data_flag = 0 if pairing_config.oob is None else 1
767
+ self.oob_data_flag = (
768
+ 1 if pairing_config.oob and pairing_config.oob.peer_data else 0
769
+ )
767
770
 
768
771
  # Set up addresses
769
- self_address = connection.self_address
772
+ self_address = connection.self_resolvable_address or connection.self_address
770
773
  peer_address = connection.peer_resolvable_address or connection.peer_address
774
+ logger.debug(
775
+ f"pairing with self_address={self_address}, peer_address={peer_address}"
776
+ )
771
777
  if self.is_initiator:
772
778
  self.ia = bytes(self_address)
773
779
  self.iat = 1 if self_address.is_random else 0
@@ -784,7 +790,7 @@ class Session:
784
790
  self.peer_oob_data = pairing_config.oob.peer_data
785
791
  if pairing_config.sc:
786
792
  if pairing_config.oob.our_context is None:
787
- raise ValueError(
793
+ raise InvalidArgumentError(
788
794
  "oob pairing config requires a context when sc is True"
789
795
  )
790
796
  self.r = pairing_config.oob.our_context.r
@@ -793,7 +799,7 @@ class Session:
793
799
  self.tk = pairing_config.oob.legacy_context.tk
794
800
  else:
795
801
  if pairing_config.oob.legacy_context is None:
796
- raise ValueError(
802
+ raise InvalidArgumentError(
797
803
  "oob pairing config requires a legacy context when sc is False"
798
804
  )
799
805
  self.r = bytes(16)
@@ -1010,8 +1016,10 @@ class Session:
1010
1016
  self.send_command(response)
1011
1017
 
1012
1018
  def send_pairing_confirm_command(self) -> None:
1013
- self.r = crypto.r()
1014
- logger.debug(f'generated random: {self.r.hex()}')
1019
+
1020
+ if self.pairing_method != PairingMethod.OOB:
1021
+ self.r = crypto.r()
1022
+ logger.debug(f'generated random: {self.r.hex()}')
1015
1023
 
1016
1024
  if self.sc:
1017
1025
 
@@ -1074,11 +1082,19 @@ class Session:
1074
1082
  )
1075
1083
 
1076
1084
  def send_identity_address_command(self) -> None:
1077
- identity_address = {
1078
- None: self.connection.self_address,
1079
- Address.PUBLIC_DEVICE_ADDRESS: self.manager.device.public_address,
1080
- Address.RANDOM_DEVICE_ADDRESS: self.manager.device.random_address,
1081
- }[self.pairing_config.identity_address_type]
1085
+ if self.pairing_config.identity_address_type == Address.PUBLIC_DEVICE_ADDRESS:
1086
+ identity_address = self.manager.device.public_address
1087
+ elif self.pairing_config.identity_address_type == Address.RANDOM_DEVICE_ADDRESS:
1088
+ identity_address = self.manager.device.static_address
1089
+ else:
1090
+ # No identity address type set. If the controller has a public address, it
1091
+ # will be more responsible to be the identity address.
1092
+ if self.manager.device.public_address != Address.ANY:
1093
+ logger.debug("No identity address type set, using PUBLIC")
1094
+ identity_address = self.manager.device.public_address
1095
+ else:
1096
+ logger.debug("No identity address type set, using RANDOM")
1097
+ identity_address = self.manager.device.static_address
1082
1098
  self.send_command(
1083
1099
  SMP_Identity_Address_Information_Command(
1084
1100
  addr_type=identity_address.address_type,
@@ -1723,7 +1739,6 @@ class Session:
1723
1739
  if self.pairing_method in (
1724
1740
  PairingMethod.JUST_WORKS,
1725
1741
  PairingMethod.NUMERIC_COMPARISON,
1726
- PairingMethod.OOB,
1727
1742
  ):
1728
1743
  ra = bytes(16)
1729
1744
  rb = ra
@@ -1731,6 +1746,22 @@ class Session:
1731
1746
  assert self.passkey
1732
1747
  ra = self.passkey.to_bytes(16, byteorder='little')
1733
1748
  rb = ra
1749
+ elif self.pairing_method == PairingMethod.OOB:
1750
+ if self.is_initiator:
1751
+ if self.peer_oob_data:
1752
+ rb = self.peer_oob_data.r
1753
+ ra = self.r
1754
+ else:
1755
+ rb = bytes(16)
1756
+ ra = self.r
1757
+ else:
1758
+ if self.peer_oob_data:
1759
+ ra = self.peer_oob_data.r
1760
+ rb = self.r
1761
+ else:
1762
+ ra = bytes(16)
1763
+ rb = self.r
1764
+
1734
1765
  else:
1735
1766
  return
1736
1767
 
bumble/snoop.py CHANGED
@@ -23,6 +23,7 @@ import datetime
23
23
  from typing import BinaryIO, Generator
24
24
  import os
25
25
 
26
+ from bumble import core
26
27
  from bumble.hci import HCI_COMMAND_PACKET, HCI_EVENT_PACKET
27
28
 
28
29
 
@@ -138,13 +139,13 @@ def create_snooper(spec: str) -> Generator[Snooper, None, None]:
138
139
 
139
140
  """
140
141
  if ':' not in spec:
141
- raise ValueError('snooper type prefix missing')
142
+ raise core.InvalidArgumentError('snooper type prefix missing')
142
143
 
143
144
  snooper_type, snooper_args = spec.split(':', maxsplit=1)
144
145
 
145
146
  if snooper_type == 'btsnoop':
146
147
  if ':' not in snooper_args:
147
- raise ValueError('I/O type for btsnoop snooper type missing')
148
+ raise core.InvalidArgumentError('I/O type for btsnoop snooper type missing')
148
149
 
149
150
  io_type, io_name = snooper_args.split(':', maxsplit=1)
150
151
  if io_type == 'file':
@@ -165,6 +166,6 @@ def create_snooper(spec: str) -> Generator[Snooper, None, None]:
165
166
  _SNOOPER_INSTANCE_COUNT -= 1
166
167
  return
167
168
 
168
- raise ValueError(f'I/O type {io_type} not supported')
169
+ raise core.InvalidArgumentError(f'I/O type {io_type} not supported')
169
170
 
170
- raise ValueError(f'snooper type {snooper_type} not found')
171
+ raise core.InvalidArgumentError(f'snooper type {snooper_type} not found')
@@ -20,7 +20,7 @@ import logging
20
20
  import os
21
21
  from typing import Optional
22
22
 
23
- from .common import Transport, AsyncPipeSink, SnoopingTransport
23
+ from .common import Transport, AsyncPipeSink, SnoopingTransport, TransportSpecError
24
24
  from ..snoop import create_snooper
25
25
 
26
26
  # -----------------------------------------------------------------------------
@@ -180,7 +180,13 @@ async def _open_transport(scheme: str, spec: Optional[str]) -> Transport:
180
180
 
181
181
  return await open_android_netsim_transport(spec)
182
182
 
183
- raise ValueError('unknown transport scheme')
183
+ if scheme == 'unix':
184
+ from .unix import open_unix_client_transport
185
+
186
+ assert spec
187
+ return await open_unix_client_transport(spec)
188
+
189
+ raise TransportSpecError('unknown transport scheme')
184
190
 
185
191
 
186
192
  # -----------------------------------------------------------------------------
@@ -20,7 +20,13 @@ import grpc.aio
20
20
 
21
21
  from typing import Optional, Union
22
22
 
23
- from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink, Transport
23
+ from .common import (
24
+ PumpedTransport,
25
+ PumpedPacketSource,
26
+ PumpedPacketSink,
27
+ Transport,
28
+ TransportSpecError,
29
+ )
24
30
 
25
31
  # pylint: disable=no-name-in-module
26
32
  from .grpc_protobuf.emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub
@@ -77,7 +83,7 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
77
83
  elif ':' in param:
78
84
  server_host, server_port = param.split(':')
79
85
  else:
80
- raise ValueError('invalid parameter')
86
+ raise TransportSpecError('invalid parameter')
81
87
 
82
88
  # Connect to the gRPC server
83
89
  server_address = f'{server_host}:{server_port}'
@@ -94,7 +100,7 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
94
100
  service = VhciForwardingServiceStub(channel)
95
101
  hci_device = HciDevice(service.attachVhci())
96
102
  else:
97
- raise ValueError('invalid mode')
103
+ raise TransportSpecError('invalid mode')
98
104
 
99
105
  # Create the transport object
100
106
  class EmulatorTransport(PumpedTransport):