bumble 0.0.195__py3-none-any.whl → 0.0.198__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 (50) 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/at.py +12 -6
  8. bumble/avc.py +8 -5
  9. bumble/avctp.py +3 -2
  10. bumble/avdtp.py +5 -1
  11. bumble/avrcp.py +2 -1
  12. bumble/codecs.py +17 -13
  13. bumble/colors.py +6 -2
  14. bumble/core.py +37 -7
  15. bumble/device.py +382 -111
  16. bumble/drivers/rtk.py +13 -8
  17. bumble/gatt.py +6 -1
  18. bumble/gatt_client.py +10 -4
  19. bumble/hci.py +50 -25
  20. bumble/hid.py +24 -28
  21. bumble/host.py +4 -0
  22. bumble/l2cap.py +24 -17
  23. bumble/link.py +8 -3
  24. bumble/profiles/ascs.py +739 -0
  25. bumble/profiles/bap.py +1 -874
  26. bumble/profiles/bass.py +440 -0
  27. bumble/profiles/csip.py +4 -4
  28. bumble/profiles/gap.py +110 -0
  29. bumble/profiles/heart_rate_service.py +4 -3
  30. bumble/profiles/le_audio.py +43 -9
  31. bumble/profiles/mcp.py +448 -0
  32. bumble/profiles/pacs.py +210 -0
  33. bumble/profiles/tmap.py +89 -0
  34. bumble/rfcomm.py +4 -2
  35. bumble/sdp.py +13 -11
  36. bumble/smp.py +20 -8
  37. bumble/snoop.py +5 -4
  38. bumble/transport/__init__.py +8 -2
  39. bumble/transport/android_emulator.py +9 -3
  40. bumble/transport/android_netsim.py +9 -7
  41. bumble/transport/common.py +46 -18
  42. bumble/transport/pyusb.py +2 -2
  43. bumble/transport/unix.py +56 -0
  44. bumble/transport/usb.py +57 -46
  45. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
  46. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/RECORD +50 -42
  47. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
  48. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
  49. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
  50. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -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/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
  )
@@ -766,8 +767,11 @@ class Session:
766
767
  self.oob_data_flag = 0 if pairing_config.oob is None else 1
767
768
 
768
769
  # Set up addresses
769
- self_address = connection.self_address
770
+ self_address = connection.self_resolvable_address or connection.self_address
770
771
  peer_address = connection.peer_resolvable_address or connection.peer_address
772
+ logger.debug(
773
+ f"pairing with self_address={self_address}, peer_address={peer_address}"
774
+ )
771
775
  if self.is_initiator:
772
776
  self.ia = bytes(self_address)
773
777
  self.iat = 1 if self_address.is_random else 0
@@ -784,7 +788,7 @@ class Session:
784
788
  self.peer_oob_data = pairing_config.oob.peer_data
785
789
  if pairing_config.sc:
786
790
  if pairing_config.oob.our_context is None:
787
- raise ValueError(
791
+ raise InvalidArgumentError(
788
792
  "oob pairing config requires a context when sc is True"
789
793
  )
790
794
  self.r = pairing_config.oob.our_context.r
@@ -793,7 +797,7 @@ class Session:
793
797
  self.tk = pairing_config.oob.legacy_context.tk
794
798
  else:
795
799
  if pairing_config.oob.legacy_context is None:
796
- raise ValueError(
800
+ raise InvalidArgumentError(
797
801
  "oob pairing config requires a legacy context when sc is False"
798
802
  )
799
803
  self.r = bytes(16)
@@ -1074,11 +1078,19 @@ class Session:
1074
1078
  )
1075
1079
 
1076
1080
  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]
1081
+ if self.pairing_config.identity_address_type == Address.PUBLIC_DEVICE_ADDRESS:
1082
+ identity_address = self.manager.device.public_address
1083
+ elif self.pairing_config.identity_address_type == Address.RANDOM_DEVICE_ADDRESS:
1084
+ identity_address = self.manager.device.static_address
1085
+ else:
1086
+ # No identity address type set. If the controller has a public address, it
1087
+ # will be more responsible to be the identity address.
1088
+ if self.manager.device.public_address != Address.ANY:
1089
+ logger.debug("No identity address type set, using PUBLIC")
1090
+ identity_address = self.manager.device.public_address
1091
+ else:
1092
+ logger.debug("No identity address type set, using RANDOM")
1093
+ identity_address = self.manager.device.static_address
1082
1094
  self.send_command(
1083
1095
  SMP_Identity_Address_Information_Command(
1084
1096
  addr_type=identity_address.address_type,
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):
@@ -31,6 +31,8 @@ from .common import (
31
31
  PumpedPacketSource,
32
32
  PumpedPacketSink,
33
33
  Transport,
34
+ TransportSpecError,
35
+ TransportInitError,
34
36
  )
35
37
 
36
38
  # pylint: disable=no-name-in-module
@@ -135,7 +137,7 @@ async def open_android_netsim_controller_transport(
135
137
  server_host: Optional[str], server_port: int, options: Dict[str, str]
136
138
  ) -> Transport:
137
139
  if not server_port:
138
- raise ValueError('invalid port')
140
+ raise TransportSpecError('invalid port')
139
141
  if server_host == '_' or not server_host:
140
142
  server_host = 'localhost'
141
143
 
@@ -288,7 +290,7 @@ async def open_android_netsim_host_transport_with_address(
288
290
  instance_number = 0 if options is None else int(options.get('instance', '0'))
289
291
  server_port = find_grpc_port(instance_number)
290
292
  if not server_port:
291
- raise RuntimeError('gRPC server port not found')
293
+ raise TransportInitError('gRPC server port not found')
292
294
 
293
295
  # Connect to the gRPC server
294
296
  server_address = f'{server_host}:{server_port}'
@@ -326,7 +328,7 @@ async def open_android_netsim_host_transport_with_channel(
326
328
 
327
329
  if response_type == 'error':
328
330
  logger.warning(f'received error: {response.error}')
329
- raise RuntimeError(response.error)
331
+ raise TransportInitError(response.error)
330
332
 
331
333
  if response_type == 'hci_packet':
332
334
  return (
@@ -334,7 +336,7 @@ async def open_android_netsim_host_transport_with_channel(
334
336
  + response.hci_packet.packet
335
337
  )
336
338
 
337
- raise ValueError('unsupported response type')
339
+ raise TransportSpecError('unsupported response type')
338
340
 
339
341
  async def write(self, packet):
340
342
  await self.hci_device.write(
@@ -429,7 +431,7 @@ async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
429
431
  options: Dict[str, str] = {}
430
432
  for param in params[params_offset:]:
431
433
  if '=' not in param:
432
- raise ValueError('invalid parameter, expected <name>=<value>')
434
+ raise TransportSpecError('invalid parameter, expected <name>=<value>')
433
435
  option_name, option_value = param.split('=')
434
436
  options[option_name] = option_value
435
437
 
@@ -440,7 +442,7 @@ async def open_android_netsim_transport(spec: Optional[str]) -> Transport:
440
442
  )
441
443
  if mode == 'controller':
442
444
  if host is None:
443
- raise ValueError('<host>:<port> missing')
445
+ raise TransportSpecError('<host>:<port> missing')
444
446
  return await open_android_netsim_controller_transport(host, port, options)
445
447
 
446
- raise ValueError('invalid mode option')
448
+ raise TransportSpecError('invalid mode option')
@@ -23,6 +23,7 @@ import logging
23
23
  import io
24
24
  from typing import Any, ContextManager, Tuple, Optional, Protocol, Dict
25
25
 
26
+ from bumble import core
26
27
  from bumble import hci
27
28
  from bumble.colors import color
28
29
  from bumble.snoop import Snooper
@@ -49,10 +50,16 @@ HCI_PACKET_INFO: Dict[int, Tuple[int, int, str]] = {
49
50
  # -----------------------------------------------------------------------------
50
51
  # Errors
51
52
  # -----------------------------------------------------------------------------
52
- class TransportLostError(Exception):
53
- """
54
- The Transport has been lost/disconnected.
55
- """
53
+ class TransportLostError(core.BaseBumbleError, RuntimeError):
54
+ """The Transport has been lost/disconnected."""
55
+
56
+
57
+ class TransportInitError(core.BaseBumbleError, RuntimeError):
58
+ """Error raised when the transport cannot be initialized."""
59
+
60
+
61
+ class TransportSpecError(core.BaseBumbleError, ValueError):
62
+ """Error raised when the transport spec is invalid."""
56
63
 
57
64
 
58
65
  # -----------------------------------------------------------------------------
@@ -132,7 +139,9 @@ class PacketParser:
132
139
  packet_type
133
140
  ) or self.extended_packet_info.get(packet_type)
134
141
  if self.packet_info is None:
135
- raise ValueError(f'invalid packet type {packet_type}')
142
+ raise core.InvalidPacketError(
143
+ f'invalid packet type {packet_type}'
144
+ )
136
145
  self.state = PacketParser.NEED_LENGTH
137
146
  self.bytes_needed = self.packet_info[0] + self.packet_info[1]
138
147
  elif self.state == PacketParser.NEED_LENGTH:
@@ -178,19 +187,19 @@ class PacketReader:
178
187
  # Get the packet info based on its type
179
188
  packet_info = HCI_PACKET_INFO.get(packet_type[0])
180
189
  if packet_info is None:
181
- raise ValueError(f'invalid packet type {packet_type[0]} found')
190
+ raise core.InvalidPacketError(f'invalid packet type {packet_type[0]} found')
182
191
 
183
192
  # Read the header (that includes the length)
184
193
  header_size = packet_info[0] + packet_info[1]
185
194
  header = self.source.read(header_size)
186
195
  if len(header) != header_size:
187
- raise ValueError('packet too short')
196
+ raise core.InvalidPacketError('packet too short')
188
197
 
189
198
  # Read the body
190
199
  body_length = struct.unpack_from(packet_info[2], header, packet_info[1])[0]
191
200
  body = self.source.read(body_length)
192
201
  if len(body) != body_length:
193
- raise ValueError('packet too short')
202
+ raise core.InvalidPacketError('packet too short')
194
203
 
195
204
  return packet_type + header + body
196
205
 
@@ -211,7 +220,7 @@ class AsyncPacketReader:
211
220
  # Get the packet info based on its type
212
221
  packet_info = HCI_PACKET_INFO.get(packet_type[0])
213
222
  if packet_info is None:
214
- raise ValueError(f'invalid packet type {packet_type[0]} found')
223
+ raise core.InvalidPacketError(f'invalid packet type {packet_type[0]} found')
215
224
 
216
225
  # Read the header (that includes the length)
217
226
  header_size = packet_info[0] + packet_info[1]
@@ -239,26 +248,28 @@ class AsyncPipeSink:
239
248
 
240
249
 
241
250
  # -----------------------------------------------------------------------------
242
- class ParserSource:
251
+ class BaseSource:
243
252
  """
244
253
  Base class designed to be subclassed by transport-specific source classes
245
254
  """
246
255
 
247
256
  terminated: asyncio.Future[None]
248
- parser: PacketParser
257
+ sink: Optional[TransportSink]
249
258
 
250
259
  def __init__(self) -> None:
251
- self.parser = PacketParser()
252
260
  self.terminated = asyncio.get_running_loop().create_future()
261
+ self.sink = None
253
262
 
254
263
  def set_packet_sink(self, sink: TransportSink) -> None:
255
- self.parser.set_packet_sink(sink)
264
+ self.sink = sink
256
265
 
257
266
  def on_transport_lost(self) -> None:
258
- self.terminated.set_result(None)
259
- if self.parser.sink:
260
- if hasattr(self.parser.sink, 'on_transport_lost'):
261
- self.parser.sink.on_transport_lost()
267
+ if not self.terminated.done():
268
+ self.terminated.set_result(None)
269
+
270
+ if self.sink:
271
+ if hasattr(self.sink, 'on_transport_lost'):
272
+ self.sink.on_transport_lost()
262
273
 
263
274
  async def wait_for_termination(self) -> None:
264
275
  """
@@ -271,6 +282,23 @@ class ParserSource:
271
282
  pass
272
283
 
273
284
 
285
+ # -----------------------------------------------------------------------------
286
+ class ParserSource(BaseSource):
287
+ """
288
+ Base class for sources that use an HCI parser.
289
+ """
290
+
291
+ parser: PacketParser
292
+
293
+ def __init__(self) -> None:
294
+ super().__init__()
295
+ self.parser = PacketParser()
296
+
297
+ def set_packet_sink(self, sink: TransportSink) -> None:
298
+ super().set_packet_sink(sink)
299
+ self.parser.set_packet_sink(sink)
300
+
301
+
274
302
  # -----------------------------------------------------------------------------
275
303
  class StreamPacketSource(asyncio.Protocol, ParserSource):
276
304
  def data_received(self, data: bytes) -> None:
@@ -420,7 +448,7 @@ class SnoopingTransport(Transport):
420
448
  return SnoopingTransport(
421
449
  transport, exit_stack.enter_context(snooper), exit_stack.pop_all().close
422
450
  )
423
- raise RuntimeError('unexpected code path') # Satisfy the type checker
451
+ raise core.UnreachableError() # Satisfy the type checker
424
452
 
425
453
  class Source:
426
454
  sink: TransportSink
bumble/transport/pyusb.py CHANGED
@@ -29,7 +29,7 @@ from usb.core import USBError
29
29
  from usb.util import CTRL_TYPE_CLASS, CTRL_RECIPIENT_OTHER
30
30
  from usb.legacy import REQ_SET_FEATURE, REQ_CLEAR_FEATURE, CLASS_HUB
31
31
 
32
- from .common import Transport, ParserSource
32
+ from .common import Transport, ParserSource, TransportInitError
33
33
  from .. import hci
34
34
  from ..colors import color
35
35
 
@@ -259,7 +259,7 @@ async def open_pyusb_transport(spec: str) -> Transport:
259
259
  device = None
260
260
 
261
261
  if device is None:
262
- raise ValueError('device not found')
262
+ raise TransportInitError('device not found')
263
263
  logger.debug(f'USB Device: {device}')
264
264
 
265
265
  # Power Cycle the device
@@ -0,0 +1,56 @@
1
+ # Copyright 2021-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 the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -----------------------------------------------------------------------------
16
+ # Imports
17
+ # -----------------------------------------------------------------------------
18
+ import asyncio
19
+ import logging
20
+
21
+ from .common import Transport, StreamPacketSource, StreamPacketSink
22
+
23
+ # -----------------------------------------------------------------------------
24
+ # Logging
25
+ # -----------------------------------------------------------------------------
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ # -----------------------------------------------------------------------------
30
+ async def open_unix_client_transport(spec: str) -> Transport:
31
+ '''Open a UNIX socket client transport.
32
+
33
+ The parameter is the path of unix socket. For abstract socket, the first character
34
+ needs to be '@'.
35
+
36
+ Example:
37
+ * /tmp/hci.socket
38
+ * @hci_socket
39
+ '''
40
+
41
+ class UnixPacketSource(StreamPacketSource):
42
+ def connection_lost(self, exc):
43
+ logger.debug(f'connection lost: {exc}')
44
+ self.on_transport_lost()
45
+
46
+ # For abstract socket, the first character should be null character.
47
+ if spec.startswith('@'):
48
+ spec = '\0' + spec[1:]
49
+
50
+ (
51
+ unix_transport,
52
+ packet_source,
53
+ ) = await asyncio.get_running_loop().create_unix_connection(UnixPacketSource, spec)
54
+ packet_sink = StreamPacketSink(unix_transport)
55
+
56
+ return Transport(packet_source, packet_sink)