bumble 0.0.180__py3-none-any.whl → 0.0.182__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 (42) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/bench.py +397 -133
  3. bumble/apps/ble_rpa_tool.py +63 -0
  4. bumble/apps/console.py +4 -4
  5. bumble/apps/controller_info.py +64 -6
  6. bumble/apps/controller_loopback.py +200 -0
  7. bumble/apps/l2cap_bridge.py +32 -24
  8. bumble/apps/pair.py +6 -8
  9. bumble/att.py +53 -11
  10. bumble/controller.py +159 -24
  11. bumble/crypto.py +10 -0
  12. bumble/device.py +580 -113
  13. bumble/drivers/__init__.py +27 -31
  14. bumble/drivers/common.py +45 -0
  15. bumble/drivers/rtk.py +11 -4
  16. bumble/gatt.py +66 -51
  17. bumble/gatt_server.py +30 -22
  18. bumble/hci.py +258 -91
  19. bumble/helpers.py +14 -0
  20. bumble/hfp.py +37 -27
  21. bumble/hid.py +282 -61
  22. bumble/host.py +158 -93
  23. bumble/l2cap.py +11 -6
  24. bumble/link.py +55 -1
  25. bumble/profiles/asha_service.py +2 -2
  26. bumble/profiles/bap.py +1247 -0
  27. bumble/profiles/cap.py +52 -0
  28. bumble/profiles/csip.py +119 -9
  29. bumble/rfcomm.py +31 -20
  30. bumble/smp.py +1 -1
  31. bumble/transport/__init__.py +51 -22
  32. bumble/transport/android_emulator.py +1 -1
  33. bumble/transport/common.py +2 -1
  34. bumble/transport/hci_socket.py +1 -4
  35. bumble/transport/usb.py +1 -1
  36. bumble/utils.py +3 -6
  37. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/METADATA +1 -1
  38. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/RECORD +42 -37
  39. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/entry_points.txt +1 -0
  40. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/LICENSE +0 -0
  41. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/WHEEL +0 -0
  42. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/top_level.txt +0 -0
bumble/profiles/cap.py ADDED
@@ -0,0 +1,52 @@
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
+
21
+ from bumble import gatt
22
+ from bumble import gatt_client
23
+ from bumble.profiles import csip
24
+
25
+
26
+ # -----------------------------------------------------------------------------
27
+ # Server
28
+ # -----------------------------------------------------------------------------
29
+ class CommonAudioServiceService(gatt.TemplateService):
30
+ UUID = gatt.GATT_COMMON_AUDIO_SERVICE
31
+
32
+ def __init__(
33
+ self,
34
+ coordinated_set_identification_service: csip.CoordinatedSetIdentificationService,
35
+ ) -> None:
36
+ self.coordinated_set_identification_service = (
37
+ coordinated_set_identification_service
38
+ )
39
+ super().__init__(
40
+ characteristics=[],
41
+ included_services=[coordinated_set_identification_service],
42
+ )
43
+
44
+
45
+ # -----------------------------------------------------------------------------
46
+ # Client
47
+ # -----------------------------------------------------------------------------
48
+ class CommonAudioServiceServiceProxy(gatt_client.ProfileServiceProxy):
49
+ SERVICE_CLASS = CommonAudioServiceService
50
+
51
+ def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
52
+ self.service_proxy = service_proxy
bumble/profiles/csip.py CHANGED
@@ -19,8 +19,11 @@
19
19
  from __future__ import annotations
20
20
  import enum
21
21
  import struct
22
- from typing import Optional
22
+ from typing import Optional, Tuple
23
23
 
24
+ from bumble import core
25
+ from bumble import crypto
26
+ from bumble import device
24
27
  from bumble import gatt
25
28
  from bumble import gatt_client
26
29
 
@@ -28,6 +31,9 @@ from bumble import gatt_client
28
31
  # -----------------------------------------------------------------------------
29
32
  # Constants
30
33
  # -----------------------------------------------------------------------------
34
+ SET_IDENTITY_RESOLVING_KEY_LENGTH = 16
35
+
36
+
31
37
  class SirkType(enum.IntEnum):
32
38
  '''Coordinated Set Identification Service - 5.1 Set Identity Resolving Key.'''
33
39
 
@@ -43,9 +49,47 @@ class MemberLock(enum.IntEnum):
43
49
 
44
50
 
45
51
  # -----------------------------------------------------------------------------
46
- # Utils
52
+ # Crypto Toolbox
47
53
  # -----------------------------------------------------------------------------
48
- # TODO: Implement RSI Generator
54
+ def s1(m: bytes) -> bytes:
55
+ '''
56
+ Coordinated Set Identification Service - 4.3 s1 SALT generation function.
57
+ '''
58
+ return crypto.aes_cmac(m[::-1], bytes(16))[::-1]
59
+
60
+
61
+ def k1(n: bytes, salt: bytes, p: bytes) -> bytes:
62
+ '''
63
+ Coordinated Set Identification Service - 4.4 k1 derivation function.
64
+ '''
65
+ t = crypto.aes_cmac(n[::-1], salt[::-1])
66
+ return crypto.aes_cmac(p[::-1], t)[::-1]
67
+
68
+
69
+ def sef(k: bytes, r: bytes) -> bytes:
70
+ '''
71
+ Coordinated Set Identification Service - 4.5 SIRK encryption function sef.
72
+
73
+ SIRK decryption function sdf shares the same algorithm. The only difference is that argument r is:
74
+ * Plaintext in encryption
75
+ * Cipher in decryption
76
+ '''
77
+ return crypto.xor(k1(k, s1(b'SIRKenc'[::-1]), b'csis'[::-1]), r)
78
+
79
+
80
+ def sih(k: bytes, r: bytes) -> bytes:
81
+ '''
82
+ Coordinated Set Identification Service - 4.7 Resolvable Set Identifier hash function sih.
83
+ '''
84
+ return crypto.e(k, r + bytes(13))[:3]
85
+
86
+
87
+ def generate_rsi(sirk: bytes) -> bytes:
88
+ '''
89
+ Coordinated Set Identification Service - 4.8 Resolvable Set Identifier generation operation.
90
+ '''
91
+ prand = crypto.generate_prand()
92
+ return sih(sirk, prand) + prand
49
93
 
50
94
 
51
95
  # -----------------------------------------------------------------------------
@@ -54,6 +98,7 @@ class MemberLock(enum.IntEnum):
54
98
  class CoordinatedSetIdentificationService(gatt.TemplateService):
55
99
  UUID = gatt.GATT_COORDINATED_SET_IDENTIFICATION_SERVICE
56
100
 
101
+ set_identity_resolving_key: bytes
57
102
  set_identity_resolving_key_characteristic: gatt.Characteristic
58
103
  coordinated_set_size_characteristic: Optional[gatt.Characteristic] = None
59
104
  set_member_lock_characteristic: Optional[gatt.Characteristic] = None
@@ -62,19 +107,26 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
62
107
  def __init__(
63
108
  self,
64
109
  set_identity_resolving_key: bytes,
110
+ set_identity_resolving_key_type: SirkType,
65
111
  coordinated_set_size: Optional[int] = None,
66
112
  set_member_lock: Optional[MemberLock] = None,
67
113
  set_member_rank: Optional[int] = None,
68
114
  ) -> None:
115
+ if len(set_identity_resolving_key) != SET_IDENTITY_RESOLVING_KEY_LENGTH:
116
+ raise ValueError(
117
+ f'Invalid SIRK length {len(set_identity_resolving_key)}, expected {SET_IDENTITY_RESOLVING_KEY_LENGTH}'
118
+ )
119
+
69
120
  characteristics = []
70
121
 
122
+ self.set_identity_resolving_key = set_identity_resolving_key
123
+ self.set_identity_resolving_key_type = set_identity_resolving_key_type
71
124
  self.set_identity_resolving_key_characteristic = gatt.Characteristic(
72
125
  uuid=gatt.GATT_SET_IDENTITY_RESOLVING_KEY_CHARACTERISTIC,
73
126
  properties=gatt.Characteristic.Properties.READ
74
127
  | 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,
128
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
129
+ value=gatt.CharacteristicValue(read=self.on_sirk_read),
78
130
  )
79
131
  characteristics.append(self.set_identity_resolving_key_characteristic)
80
132
 
@@ -83,7 +135,7 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
83
135
  uuid=gatt.GATT_COORDINATED_SET_SIZE_CHARACTERISTIC,
84
136
  properties=gatt.Characteristic.Properties.READ
85
137
  | gatt.Characteristic.Properties.NOTIFY,
86
- permissions=gatt.Characteristic.Permissions.READABLE,
138
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
87
139
  value=struct.pack('B', coordinated_set_size),
88
140
  )
89
141
  characteristics.append(self.coordinated_set_size_characteristic)
@@ -94,7 +146,7 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
94
146
  properties=gatt.Characteristic.Properties.READ
95
147
  | gatt.Characteristic.Properties.NOTIFY
96
148
  | gatt.Characteristic.Properties.WRITE,
97
- permissions=gatt.Characteristic.Permissions.READABLE
149
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
98
150
  | gatt.Characteristic.Permissions.WRITEABLE,
99
151
  value=struct.pack('B', set_member_lock),
100
152
  )
@@ -105,13 +157,45 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
105
157
  uuid=gatt.GATT_SET_MEMBER_RANK_CHARACTERISTIC,
106
158
  properties=gatt.Characteristic.Properties.READ
107
159
  | gatt.Characteristic.Properties.NOTIFY,
108
- permissions=gatt.Characteristic.Permissions.READABLE,
160
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
109
161
  value=struct.pack('B', set_member_rank),
110
162
  )
111
163
  characteristics.append(self.set_member_rank_characteristic)
112
164
 
113
165
  super().__init__(characteristics)
114
166
 
167
+ async def on_sirk_read(self, connection: Optional[device.Connection]) -> bytes:
168
+ if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
169
+ sirk_bytes = self.set_identity_resolving_key
170
+ else:
171
+ assert connection
172
+
173
+ if connection.transport == core.BT_LE_TRANSPORT:
174
+ key = await connection.device.get_long_term_key(
175
+ connection_handle=connection.handle, rand=b'', ediv=0
176
+ )
177
+ else:
178
+ key = await connection.device.get_link_key(connection.peer_address)
179
+
180
+ if not key:
181
+ raise RuntimeError('LTK or LinkKey is not present')
182
+
183
+ sirk_bytes = sef(key, self.set_identity_resolving_key)
184
+
185
+ return bytes([self.set_identity_resolving_key_type]) + sirk_bytes
186
+
187
+ def get_advertising_data(self) -> bytes:
188
+ return bytes(
189
+ core.AdvertisingData(
190
+ [
191
+ (
192
+ core.AdvertisingData.RESOLVABLE_SET_IDENTIFIER,
193
+ generate_rsi(self.set_identity_resolving_key),
194
+ ),
195
+ ]
196
+ )
197
+ )
198
+
115
199
 
116
200
  # -----------------------------------------------------------------------------
117
201
  # Client
@@ -145,3 +229,29 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
145
229
  gatt.GATT_SET_MEMBER_RANK_CHARACTERISTIC
146
230
  ):
147
231
  self.set_member_rank = characteristics[0]
232
+
233
+ async def read_set_identity_resolving_key(self) -> Tuple[SirkType, bytes]:
234
+ '''Reads SIRK and decrypts if encrypted.'''
235
+ response = await self.set_identity_resolving_key.read_value()
236
+ if len(response) != SET_IDENTITY_RESOLVING_KEY_LENGTH + 1:
237
+ raise RuntimeError('Invalid SIRK value')
238
+
239
+ sirk_type = SirkType(response[0])
240
+ if sirk_type == SirkType.PLAINTEXT:
241
+ sirk = response[1:]
242
+ else:
243
+ connection = self.service_proxy.client.connection
244
+ device = connection.device
245
+ if connection.transport == core.BT_LE_TRANSPORT:
246
+ key = await device.get_long_term_key(
247
+ connection_handle=connection.handle, rand=b'', ediv=0
248
+ )
249
+ else:
250
+ key = await device.get_link_key(connection.peer_address)
251
+
252
+ if not key:
253
+ raise RuntimeError('LTK or LinkKey is not present')
254
+
255
+ sirk = sef(key, response[1:])
256
+
257
+ return (sirk_type, sirk)
bumble/rfcomm.py CHANGED
@@ -118,8 +118,8 @@ CRC_TABLE = bytes([
118
118
  0XBA, 0X2B, 0X59, 0XC8, 0XBD, 0X2C, 0X5E, 0XCF
119
119
  ])
120
120
 
121
- RFCOMM_DEFAULT_INITIAL_RX_CREDITS = 7
122
- RFCOMM_DEFAULT_PREFERRED_MTU = 1280
121
+ RFCOMM_DEFAULT_WINDOW_SIZE = 16
122
+ RFCOMM_DEFAULT_MAX_FRAME_SIZE = 2000
123
123
 
124
124
  RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1
125
125
  RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
@@ -438,20 +438,24 @@ class DLC(EventEmitter):
438
438
  multiplexer: Multiplexer,
439
439
  dlci: int,
440
440
  max_frame_size: int,
441
- initial_tx_credits: int,
441
+ window_size: int,
442
442
  ) -> None:
443
443
  super().__init__()
444
444
  self.multiplexer = multiplexer
445
445
  self.dlci = dlci
446
- self.rx_credits = RFCOMM_DEFAULT_INITIAL_RX_CREDITS
447
- self.rx_threshold = self.rx_credits // 2
448
- self.tx_credits = initial_tx_credits
446
+ self.max_frame_size = max_frame_size
447
+ self.window_size = window_size
448
+ self.rx_credits = window_size
449
+ self.rx_threshold = window_size // 2
450
+ self.tx_credits = window_size
449
451
  self.tx_buffer = b''
450
452
  self.state = DLC.State.INIT
451
453
  self.role = multiplexer.role
452
454
  self.c_r = 1 if self.role == Multiplexer.Role.INITIATOR else 0
453
455
  self.sink = None
454
456
  self.connection_result = None
457
+ self.drained = asyncio.Event()
458
+ self.drained.set()
455
459
 
456
460
  # Compute the MTU
457
461
  max_overhead = 4 + 1 # header with 2-byte length + fcs
@@ -537,11 +541,11 @@ class DLC(EventEmitter):
537
541
  if len(data) and self.sink:
538
542
  self.sink(data) # pylint: disable=not-callable
539
543
 
540
- # Update the credits
541
- if self.rx_credits > 0:
542
- self.rx_credits -= 1
543
- else:
544
- logger.warning(color('!!! received frame with no rx credits', 'red'))
544
+ # Update the credits
545
+ if self.rx_credits > 0:
546
+ self.rx_credits -= 1
547
+ else:
548
+ logger.warning(color('!!! received frame with no rx credits', 'red'))
545
549
 
546
550
  # Check if there's anything to send (including credits)
547
551
  self.process_tx()
@@ -580,9 +584,9 @@ class DLC(EventEmitter):
580
584
  cl=0xE0,
581
585
  priority=7,
582
586
  ack_timer=0,
583
- max_frame_size=RFCOMM_DEFAULT_PREFERRED_MTU,
587
+ max_frame_size=self.max_frame_size,
584
588
  max_retransmissions=0,
585
- window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
589
+ window_size=self.window_size,
586
590
  )
587
591
  mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=0, data=bytes(pn))
588
592
  logger.debug(f'>>> PN Response: {pn}')
@@ -591,7 +595,7 @@ class DLC(EventEmitter):
591
595
 
592
596
  def rx_credits_needed(self) -> int:
593
597
  if self.rx_credits <= self.rx_threshold:
594
- return RFCOMM_DEFAULT_INITIAL_RX_CREDITS - self.rx_credits
598
+ return self.window_size - self.rx_credits
595
599
 
596
600
  return 0
597
601
 
@@ -631,6 +635,8 @@ class DLC(EventEmitter):
631
635
  )
632
636
 
633
637
  rx_credits_needed = 0
638
+ if not self.tx_buffer:
639
+ self.drained.set()
634
640
 
635
641
  # Stream protocol
636
642
  def write(self, data: Union[bytes, str]) -> None:
@@ -643,11 +649,11 @@ class DLC(EventEmitter):
643
649
  raise ValueError('write only accept bytes or strings')
644
650
 
645
651
  self.tx_buffer += data
652
+ self.drained.clear()
646
653
  self.process_tx()
647
654
 
648
- def drain(self) -> None:
649
- # TODO
650
- pass
655
+ async def drain(self) -> None:
656
+ await self.drained.wait()
651
657
 
652
658
  def __str__(self) -> str:
653
659
  return f'DLC(dlci={self.dlci},state={self.state.name})'
@@ -843,7 +849,12 @@ class Multiplexer(EventEmitter):
843
849
  )
844
850
  await self.disconnection_result
845
851
 
846
- async def open_dlc(self, channel: int) -> DLC:
852
+ async def open_dlc(
853
+ self,
854
+ channel: int,
855
+ max_frame_size: int = RFCOMM_DEFAULT_MAX_FRAME_SIZE,
856
+ window_size: int = RFCOMM_DEFAULT_WINDOW_SIZE,
857
+ ) -> DLC:
847
858
  if self.state != Multiplexer.State.CONNECTED:
848
859
  if self.state == Multiplexer.State.OPENING:
849
860
  raise InvalidStateError('open already in progress')
@@ -855,9 +866,9 @@ class Multiplexer(EventEmitter):
855
866
  cl=0xF0,
856
867
  priority=7,
857
868
  ack_timer=0,
858
- max_frame_size=RFCOMM_DEFAULT_PREFERRED_MTU,
869
+ max_frame_size=max_frame_size,
859
870
  max_retransmissions=0,
860
- window_size=RFCOMM_DEFAULT_INITIAL_RX_CREDITS,
871
+ window_size=window_size,
861
872
  )
862
873
  mcc = RFCOMM_Frame.make_mcc(mcc_type=RFCOMM_MCC_PN_TYPE, c_r=1, data=bytes(pn))
863
874
  logger.debug(f'>>> Sending MCC: {pn}')
bumble/smp.py CHANGED
@@ -1090,7 +1090,7 @@ class Session:
1090
1090
  # We can now encrypt the connection with the short term key, so that we can
1091
1091
  # distribute the long term and/or other keys over an encrypted connection
1092
1092
  self.manager.device.host.send_command_sync(
1093
- HCI_LE_Enable_Encryption_Command( # type: ignore[call-arg]
1093
+ HCI_LE_Enable_Encryption_Command(
1094
1094
  connection_handle=self.connection.handle,
1095
1095
  random_number=bytes(8),
1096
1096
  encrypted_diversifier=0,
@@ -18,6 +18,7 @@
18
18
  from contextlib import asynccontextmanager
19
19
  import logging
20
20
  import os
21
+ from typing import Optional
21
22
 
22
23
  from .common import Transport, AsyncPipeSink, SnoopingTransport
23
24
  from ..snoop import create_snooper
@@ -52,8 +53,16 @@ def _wrap_transport(transport: Transport) -> Transport:
52
53
  async def open_transport(name: str) -> Transport:
53
54
  """
54
55
  Open a transport by name.
55
- The name must be <type>:<parameters>
56
- Where <parameters> depend on the type (and may be empty for some types).
56
+ The name must be <type>:<metadata><parameters>
57
+ Where <parameters> depend on the type (and may be empty for some types), and
58
+ <metadata> is either omitted, or a ,-separated list of <key>=<value> pairs,
59
+ enclosed in [].
60
+ If there are not metadata or parameter, the : after the <type> may be omitted.
61
+ Examples:
62
+ * usb:0
63
+ * usb:[driver=rtk]0
64
+ * android-netsim
65
+
57
66
  The supported types are:
58
67
  * serial
59
68
  * udp
@@ -71,87 +80,106 @@ async def open_transport(name: str) -> Transport:
71
80
  * android-netsim
72
81
  """
73
82
 
74
- return _wrap_transport(await _open_transport(name))
83
+ scheme, *tail = name.split(':', 1)
84
+ spec = tail[0] if tail else None
85
+ if spec:
86
+ # Metadata may precede the spec
87
+ if spec.startswith('['):
88
+ metadata_str, *tail = spec[1:].split(']')
89
+ spec = tail[0] if tail else None
90
+ metadata = dict([entry.split('=') for entry in metadata_str.split(',')])
91
+ else:
92
+ metadata = None
93
+
94
+ transport = await _open_transport(scheme, spec)
95
+ if metadata:
96
+ transport.source.metadata = { # type: ignore[attr-defined]
97
+ **metadata,
98
+ **getattr(transport.source, 'metadata', {}),
99
+ }
100
+ # pylint: disable=line-too-long
101
+ logger.debug(f'HCI metadata: {transport.source.metadata}') # type: ignore[attr-defined]
102
+
103
+ return _wrap_transport(transport)
75
104
 
76
105
 
77
106
  # -----------------------------------------------------------------------------
78
- async def _open_transport(name: str) -> Transport:
107
+ async def _open_transport(scheme: str, spec: Optional[str]) -> Transport:
79
108
  # pylint: disable=import-outside-toplevel
80
109
  # pylint: disable=too-many-return-statements
81
110
 
82
- scheme, *spec = name.split(':', 1)
83
111
  if scheme == 'serial' and spec:
84
112
  from .serial import open_serial_transport
85
113
 
86
- return await open_serial_transport(spec[0])
114
+ return await open_serial_transport(spec)
87
115
 
88
116
  if scheme == 'udp' and spec:
89
117
  from .udp import open_udp_transport
90
118
 
91
- return await open_udp_transport(spec[0])
119
+ return await open_udp_transport(spec)
92
120
 
93
121
  if scheme == 'tcp-client' and spec:
94
122
  from .tcp_client import open_tcp_client_transport
95
123
 
96
- return await open_tcp_client_transport(spec[0])
124
+ return await open_tcp_client_transport(spec)
97
125
 
98
126
  if scheme == 'tcp-server' and spec:
99
127
  from .tcp_server import open_tcp_server_transport
100
128
 
101
- return await open_tcp_server_transport(spec[0])
129
+ return await open_tcp_server_transport(spec)
102
130
 
103
131
  if scheme == 'ws-client' and spec:
104
132
  from .ws_client import open_ws_client_transport
105
133
 
106
- return await open_ws_client_transport(spec[0])
134
+ return await open_ws_client_transport(spec)
107
135
 
108
136
  if scheme == 'ws-server' and spec:
109
137
  from .ws_server import open_ws_server_transport
110
138
 
111
- return await open_ws_server_transport(spec[0])
139
+ return await open_ws_server_transport(spec)
112
140
 
113
141
  if scheme == 'pty':
114
142
  from .pty import open_pty_transport
115
143
 
116
- return await open_pty_transport(spec[0] if spec else None)
144
+ return await open_pty_transport(spec)
117
145
 
118
146
  if scheme == 'file':
119
147
  from .file import open_file_transport
120
148
 
121
149
  assert spec is not None
122
- return await open_file_transport(spec[0])
150
+ return await open_file_transport(spec)
123
151
 
124
152
  if scheme == 'vhci':
125
153
  from .vhci import open_vhci_transport
126
154
 
127
- return await open_vhci_transport(spec[0] if spec else None)
155
+ return await open_vhci_transport(spec)
128
156
 
129
157
  if scheme == 'hci-socket':
130
158
  from .hci_socket import open_hci_socket_transport
131
159
 
132
- return await open_hci_socket_transport(spec[0] if spec else None)
160
+ return await open_hci_socket_transport(spec)
133
161
 
134
162
  if scheme == 'usb':
135
163
  from .usb import open_usb_transport
136
164
 
137
- assert spec is not None
138
- return await open_usb_transport(spec[0])
165
+ assert spec
166
+ return await open_usb_transport(spec)
139
167
 
140
168
  if scheme == 'pyusb':
141
169
  from .pyusb import open_pyusb_transport
142
170
 
143
- assert spec is not None
144
- return await open_pyusb_transport(spec[0])
171
+ assert spec
172
+ return await open_pyusb_transport(spec)
145
173
 
146
174
  if scheme == 'android-emulator':
147
175
  from .android_emulator import open_android_emulator_transport
148
176
 
149
- return await open_android_emulator_transport(spec[0] if spec else None)
177
+ return await open_android_emulator_transport(spec)
150
178
 
151
179
  if scheme == 'android-netsim':
152
180
  from .android_netsim import open_android_netsim_transport
153
181
 
154
- return await open_android_netsim_transport(spec[0] if spec else None)
182
+ return await open_android_netsim_transport(spec)
155
183
 
156
184
  raise ValueError('unknown transport scheme')
157
185
 
@@ -170,12 +198,13 @@ async def open_transport_or_link(name: str) -> Transport:
170
198
 
171
199
  """
172
200
  if name.startswith('link-relay:'):
201
+ logger.warning('Link Relay has been deprecated.')
173
202
  from ..controller import Controller
174
203
  from ..link import RemoteLink # lazy import
175
204
 
176
205
  link = RemoteLink(name[11:])
177
206
  await link.wait_until_connected()
178
- controller = Controller('remote', link=link)
207
+ controller = Controller('remote', link=link) # type:ignore[arg-type]
179
208
 
180
209
  class LinkTransport(Transport):
181
210
  async def close(self):
@@ -69,7 +69,7 @@ async def open_android_emulator_transport(spec: Optional[str]) -> Transport:
69
69
  mode = 'host'
70
70
  server_host = 'localhost'
71
71
  server_port = '8554'
72
- if spec is not None:
72
+ if spec:
73
73
  params = spec.split(',')
74
74
  for param in params:
75
75
  if param.startswith('mode='):
@@ -21,7 +21,7 @@ import struct
21
21
  import asyncio
22
22
  import logging
23
23
  import io
24
- from typing import ContextManager, Tuple, Optional, Protocol, Dict
24
+ from typing import Any, ContextManager, Tuple, Optional, Protocol, Dict
25
25
 
26
26
  from bumble import hci
27
27
  from bumble.colors import color
@@ -42,6 +42,7 @@ HCI_PACKET_INFO: Dict[int, Tuple[int, int, str]] = {
42
42
  hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'),
43
43
  hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'),
44
44
  hci.HCI_EVENT_PACKET: (1, 1, 'B'),
45
+ hci.HCI_ISO_DATA_PACKET: (2, 2, 'H'),
45
46
  }
46
47
 
47
48
 
@@ -59,10 +59,7 @@ async def open_hci_socket_transport(spec: Optional[str]) -> Transport:
59
59
  ) from error
60
60
 
61
61
  # Compute the adapter index
62
- if spec is None:
63
- adapter_index = 0
64
- else:
65
- adapter_index = int(spec)
62
+ adapter_index = int(spec) if spec else 0
66
63
 
67
64
  # Bind the socket
68
65
  # NOTE: since Python doesn't support binding with the required address format (yet),
bumble/transport/usb.py CHANGED
@@ -108,7 +108,7 @@ async def open_usb_transport(spec: str) -> Transport:
108
108
  USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER,
109
109
  )
110
110
 
111
- READ_SIZE = 1024
111
+ READ_SIZE = 4096
112
112
 
113
113
  class UsbPacketSink:
114
114
  def __init__(self, device, acl_out):
bumble/utils.py CHANGED
@@ -280,17 +280,14 @@ class AsyncRunner:
280
280
  def wrapper(*args, **kwargs):
281
281
  coroutine = func(*args, **kwargs)
282
282
  if queue is None:
283
- # Create a task to run the coroutine
283
+ # Spawn the coroutine as a task
284
284
  async def run():
285
285
  try:
286
286
  await coroutine
287
287
  except Exception:
288
- logger.warning(
289
- f'{color("!!! Exception in wrapper:", "red")} '
290
- f'{traceback.format_exc()}'
291
- )
288
+ logger.exception(color("!!! Exception in wrapper:", "red"))
292
289
 
293
- asyncio.create_task(run())
290
+ AsyncRunner.spawn(run())
294
291
  else:
295
292
  # Queue the coroutine to be awaited by the work queue
296
293
  queue.enqueue(coroutine)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bumble
3
- Version: 0.0.180
3
+ Version: 0.0.182
4
4
  Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
5
5
  Home-page: https://github.com/google/bumble
6
6
  Author: Google