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.
- bumble/_version.py +2 -2
- bumble/apps/auracast.py +351 -66
- bumble/apps/console.py +5 -20
- bumble/apps/device_info.py +230 -0
- bumble/apps/gatt_dump.py +4 -0
- bumble/apps/lea_unicast/app.py +16 -17
- bumble/at.py +12 -6
- bumble/avc.py +8 -5
- bumble/avctp.py +3 -2
- bumble/avdtp.py +5 -1
- bumble/avrcp.py +2 -1
- bumble/codecs.py +17 -13
- bumble/colors.py +6 -2
- bumble/core.py +37 -7
- bumble/device.py +382 -111
- bumble/drivers/rtk.py +13 -8
- bumble/gatt.py +6 -1
- bumble/gatt_client.py +10 -4
- bumble/hci.py +50 -25
- bumble/hid.py +24 -28
- bumble/host.py +4 -0
- bumble/l2cap.py +24 -17
- bumble/link.py +8 -3
- bumble/profiles/ascs.py +739 -0
- bumble/profiles/bap.py +1 -874
- bumble/profiles/bass.py +440 -0
- bumble/profiles/csip.py +4 -4
- bumble/profiles/gap.py +110 -0
- bumble/profiles/heart_rate_service.py +4 -3
- bumble/profiles/le_audio.py +43 -9
- bumble/profiles/mcp.py +448 -0
- bumble/profiles/pacs.py +210 -0
- bumble/profiles/tmap.py +89 -0
- bumble/rfcomm.py +4 -2
- bumble/sdp.py +13 -11
- bumble/smp.py +20 -8
- bumble/snoop.py +5 -4
- bumble/transport/__init__.py +8 -2
- bumble/transport/android_emulator.py +9 -3
- bumble/transport/android_netsim.py +9 -7
- bumble/transport/common.py +46 -18
- bumble/transport/pyusb.py +2 -2
- bumble/transport/unix.py +56 -0
- bumble/transport/usb.py +57 -46
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/RECORD +50 -42
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
bumble/profiles/tmap.py
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
432
|
+
raise InvalidArgumentError('invalid data size')
|
|
431
433
|
elif self.type == DataElement.BOOLEAN:
|
|
432
434
|
if size != 1:
|
|
433
|
-
raise
|
|
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
|
|
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
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
|
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
|
|
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
|
|
169
|
+
raise core.InvalidArgumentError(f'I/O type {io_type} not supported')
|
|
169
170
|
|
|
170
|
-
raise
|
|
171
|
+
raise core.InvalidArgumentError(f'snooper type {snooper_type} not found')
|
bumble/transport/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
445
|
+
raise TransportSpecError('<host>:<port> missing')
|
|
444
446
|
return await open_android_netsim_controller_transport(host, port, options)
|
|
445
447
|
|
|
446
|
-
raise
|
|
448
|
+
raise TransportSpecError('invalid mode option')
|
bumble/transport/common.py
CHANGED
|
@@ -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(
|
|
53
|
-
"""
|
|
54
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
264
|
+
self.sink = sink
|
|
256
265
|
|
|
257
266
|
def on_transport_lost(self) -> None:
|
|
258
|
-
self.terminated.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
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
|
|
262
|
+
raise TransportInitError('device not found')
|
|
263
263
|
logger.debug(f'USB Device: {device}')
|
|
264
264
|
|
|
265
265
|
# Power Cycle the device
|
bumble/transport/unix.py
ADDED
|
@@ -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)
|