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.
- 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/apps/pair.py +32 -5
- bumble/at.py +12 -6
- bumble/att.py +56 -40
- bumble/avc.py +8 -5
- bumble/avctp.py +3 -2
- bumble/avdtp.py +7 -3
- bumble/avrcp.py +2 -1
- bumble/codecs.py +17 -13
- bumble/colors.py +6 -2
- bumble/core.py +37 -7
- bumble/decoder.py +14 -10
- bumble/device.py +382 -111
- bumble/drivers/rtk.py +32 -13
- bumble/gatt.py +30 -20
- bumble/gatt_client.py +15 -29
- bumble/gatt_server.py +14 -6
- bumble/hci.py +322 -32
- bumble/hid.py +24 -28
- bumble/host.py +20 -6
- bumble/l2cap.py +24 -17
- bumble/link.py +8 -3
- bumble/pandora/__init__.py +3 -0
- bumble/pandora/l2cap.py +310 -0
- bumble/profiles/aics.py +520 -0
- bumble/profiles/ascs.py +739 -0
- bumble/profiles/asha.py +295 -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/hap.py +665 -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/profiles/vcp.py +5 -3
- bumble/rfcomm.py +4 -2
- bumble/sdp.py +13 -11
- bumble/smp.py +43 -12
- 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 +21 -4
- bumble/transport/unix.py +56 -0
- bumble/transport/usb.py +57 -46
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/METADATA +41 -41
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/RECORD +60 -49
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/WHEEL +1 -1
- bumble/profiles/asha_service.py +0 -193
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/LICENSE +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/top_level.txt +0 -0
bumble/profiles/pacs.py
ADDED
|
@@ -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]
|
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/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
|
|
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
|
)
|
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
|
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):
|