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
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
# -----------------------------------------------------------------------------
|
|
16
|
+
# Imports
|
|
17
|
+
# -----------------------------------------------------------------------------
|
|
18
|
+
import asyncio
|
|
19
|
+
import os
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Callable, Iterable, Optional
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
|
|
25
|
+
from bumble.core import ProtocolError
|
|
26
|
+
from bumble.colors import color
|
|
27
|
+
from bumble.device import Device, Peer
|
|
28
|
+
from bumble.gatt import Service
|
|
29
|
+
from bumble.profiles.device_information_service import DeviceInformationServiceProxy
|
|
30
|
+
from bumble.profiles.battery_service import BatteryServiceProxy
|
|
31
|
+
from bumble.profiles.gap import GenericAccessServiceProxy
|
|
32
|
+
from bumble.profiles.tmap import TelephonyAndMediaAudioServiceProxy
|
|
33
|
+
from bumble.transport import open_transport_or_link
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# -----------------------------------------------------------------------------
|
|
37
|
+
async def try_show(function: Callable, *args, **kwargs) -> None:
|
|
38
|
+
try:
|
|
39
|
+
await function(*args, **kwargs)
|
|
40
|
+
except ProtocolError as error:
|
|
41
|
+
print(color('ERROR:', 'red'), error)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# -----------------------------------------------------------------------------
|
|
45
|
+
def show_services(services: Iterable[Service]) -> None:
|
|
46
|
+
for service in services:
|
|
47
|
+
print(color(str(service), 'cyan'))
|
|
48
|
+
|
|
49
|
+
for characteristic in service.characteristics:
|
|
50
|
+
print(color(' ' + str(characteristic), 'magenta'))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# -----------------------------------------------------------------------------
|
|
54
|
+
async def show_gap_information(
|
|
55
|
+
gap_service: GenericAccessServiceProxy,
|
|
56
|
+
):
|
|
57
|
+
print(color('### Generic Access Profile', 'yellow'))
|
|
58
|
+
|
|
59
|
+
if gap_service.device_name:
|
|
60
|
+
print(
|
|
61
|
+
color(' Device Name:', 'green'),
|
|
62
|
+
await gap_service.device_name.read_value(),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if gap_service.appearance:
|
|
66
|
+
print(
|
|
67
|
+
color(' Appearance: ', 'green'),
|
|
68
|
+
await gap_service.appearance.read_value(),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
print()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# -----------------------------------------------------------------------------
|
|
75
|
+
async def show_device_information(
|
|
76
|
+
device_information_service: DeviceInformationServiceProxy,
|
|
77
|
+
):
|
|
78
|
+
print(color('### Device Information', 'yellow'))
|
|
79
|
+
|
|
80
|
+
if device_information_service.manufacturer_name:
|
|
81
|
+
print(
|
|
82
|
+
color(' Manufacturer Name:', 'green'),
|
|
83
|
+
await device_information_service.manufacturer_name.read_value(),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if device_information_service.model_number:
|
|
87
|
+
print(
|
|
88
|
+
color(' Model Number: ', 'green'),
|
|
89
|
+
await device_information_service.model_number.read_value(),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if device_information_service.serial_number:
|
|
93
|
+
print(
|
|
94
|
+
color(' Serial Number: ', 'green'),
|
|
95
|
+
await device_information_service.serial_number.read_value(),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if device_information_service.firmware_revision:
|
|
99
|
+
print(
|
|
100
|
+
color(' Firmware Revision:', 'green'),
|
|
101
|
+
await device_information_service.firmware_revision.read_value(),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
print()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# -----------------------------------------------------------------------------
|
|
108
|
+
async def show_battery_level(
|
|
109
|
+
battery_service: BatteryServiceProxy,
|
|
110
|
+
):
|
|
111
|
+
print(color('### Battery Information', 'yellow'))
|
|
112
|
+
|
|
113
|
+
if battery_service.battery_level:
|
|
114
|
+
print(
|
|
115
|
+
color(' Battery Level:', 'green'),
|
|
116
|
+
await battery_service.battery_level.read_value(),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
print()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# -----------------------------------------------------------------------------
|
|
123
|
+
async def show_tmas(
|
|
124
|
+
tmas: TelephonyAndMediaAudioServiceProxy,
|
|
125
|
+
):
|
|
126
|
+
print(color('### Telephony And Media Audio Service', 'yellow'))
|
|
127
|
+
|
|
128
|
+
if tmas.role:
|
|
129
|
+
print(
|
|
130
|
+
color(' Role:', 'green'),
|
|
131
|
+
await tmas.role.read_value(),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
print()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# -----------------------------------------------------------------------------
|
|
138
|
+
async def show_device_info(peer, done: Optional[asyncio.Future]) -> None:
|
|
139
|
+
try:
|
|
140
|
+
# Discover all services
|
|
141
|
+
print(color('### Discovering Services and Characteristics', 'magenta'))
|
|
142
|
+
await peer.discover_services()
|
|
143
|
+
for service in peer.services:
|
|
144
|
+
await service.discover_characteristics()
|
|
145
|
+
|
|
146
|
+
print(color('=== Services ===', 'yellow'))
|
|
147
|
+
show_services(peer.services)
|
|
148
|
+
print()
|
|
149
|
+
|
|
150
|
+
if gap_service := peer.create_service_proxy(GenericAccessServiceProxy):
|
|
151
|
+
await try_show(show_gap_information, gap_service)
|
|
152
|
+
|
|
153
|
+
if device_information_service := peer.create_service_proxy(
|
|
154
|
+
DeviceInformationServiceProxy
|
|
155
|
+
):
|
|
156
|
+
await try_show(show_device_information, device_information_service)
|
|
157
|
+
|
|
158
|
+
if battery_service := peer.create_service_proxy(BatteryServiceProxy):
|
|
159
|
+
await try_show(show_battery_level, battery_service)
|
|
160
|
+
|
|
161
|
+
if tmas := peer.create_service_proxy(TelephonyAndMediaAudioServiceProxy):
|
|
162
|
+
await try_show(show_tmas, tmas)
|
|
163
|
+
|
|
164
|
+
if done is not None:
|
|
165
|
+
done.set_result(None)
|
|
166
|
+
except asyncio.CancelledError:
|
|
167
|
+
print(color('!!! Operation canceled', 'red'))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# -----------------------------------------------------------------------------
|
|
171
|
+
async def async_main(device_config, encrypt, transport, address_or_name):
|
|
172
|
+
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
|
173
|
+
|
|
174
|
+
# Create a device
|
|
175
|
+
if device_config:
|
|
176
|
+
device = Device.from_config_file_with_hci(
|
|
177
|
+
device_config, hci_source, hci_sink
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
device = Device.with_hci(
|
|
181
|
+
'Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink
|
|
182
|
+
)
|
|
183
|
+
await device.power_on()
|
|
184
|
+
|
|
185
|
+
if address_or_name:
|
|
186
|
+
# Connect to the target peer
|
|
187
|
+
print(color('>>> Connecting...', 'green'))
|
|
188
|
+
connection = await device.connect(address_or_name)
|
|
189
|
+
print(color('>>> Connected', 'green'))
|
|
190
|
+
|
|
191
|
+
# Encrypt the connection if required
|
|
192
|
+
if encrypt:
|
|
193
|
+
print(color('+++ Encrypting connection...', 'blue'))
|
|
194
|
+
await connection.encrypt()
|
|
195
|
+
print(color('+++ Encryption established', 'blue'))
|
|
196
|
+
|
|
197
|
+
await show_device_info(Peer(connection), None)
|
|
198
|
+
else:
|
|
199
|
+
# Wait for a connection
|
|
200
|
+
done = asyncio.get_running_loop().create_future()
|
|
201
|
+
device.on(
|
|
202
|
+
'connection',
|
|
203
|
+
lambda connection: asyncio.create_task(
|
|
204
|
+
show_device_info(Peer(connection), done)
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
await device.start_advertising(auto_restart=True)
|
|
208
|
+
|
|
209
|
+
print(color('### Waiting for connection...', 'blue'))
|
|
210
|
+
await done
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# -----------------------------------------------------------------------------
|
|
214
|
+
@click.command()
|
|
215
|
+
@click.option('--device-config', help='Device configuration', type=click.Path())
|
|
216
|
+
@click.option('--encrypt', help='Encrypt the connection', is_flag=True, default=False)
|
|
217
|
+
@click.argument('transport')
|
|
218
|
+
@click.argument('address-or-name', required=False)
|
|
219
|
+
def main(device_config, encrypt, transport, address_or_name):
|
|
220
|
+
"""
|
|
221
|
+
Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified,
|
|
222
|
+
wait for an incoming connection.
|
|
223
|
+
"""
|
|
224
|
+
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
|
225
|
+
asyncio.run(async_main(device_config, encrypt, transport, address_or_name))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# -----------------------------------------------------------------------------
|
|
229
|
+
if __name__ == '__main__':
|
|
230
|
+
main()
|
bumble/apps/gatt_dump.py
CHANGED
|
@@ -75,11 +75,15 @@ async def async_main(device_config, encrypt, transport, address_or_name):
|
|
|
75
75
|
|
|
76
76
|
if address_or_name:
|
|
77
77
|
# Connect to the target peer
|
|
78
|
+
print(color('>>> Connecting...', 'green'))
|
|
78
79
|
connection = await device.connect(address_or_name)
|
|
80
|
+
print(color('>>> Connected', 'green'))
|
|
79
81
|
|
|
80
82
|
# Encrypt the connection if required
|
|
81
83
|
if encrypt:
|
|
84
|
+
print(color('+++ Encrypting connection...', 'blue'))
|
|
82
85
|
await connection.encrypt()
|
|
86
|
+
print(color('+++ Encryption established', 'blue'))
|
|
83
87
|
|
|
84
88
|
await dump_gatt_db(Peer(connection), None)
|
|
85
89
|
else:
|
bumble/apps/lea_unicast/app.py
CHANGED
|
@@ -33,7 +33,6 @@ import ctypes
|
|
|
33
33
|
import wasmtime
|
|
34
34
|
import wasmtime.loader
|
|
35
35
|
import liblc3 # type: ignore
|
|
36
|
-
import logging
|
|
37
36
|
|
|
38
37
|
import click
|
|
39
38
|
import aiohttp.web
|
|
@@ -43,7 +42,7 @@ from bumble.core import AdvertisingData
|
|
|
43
42
|
from bumble.colors import color
|
|
44
43
|
from bumble.device import Device, DeviceConfiguration, AdvertisingParameters
|
|
45
44
|
from bumble.transport import open_transport
|
|
46
|
-
from bumble.profiles import bap
|
|
45
|
+
from bumble.profiles import ascs, bap, pacs
|
|
47
46
|
from bumble.hci import Address, CodecID, CodingFormat, HCI_IsoDataPacket
|
|
48
47
|
|
|
49
48
|
# -----------------------------------------------------------------------------
|
|
@@ -57,8 +56,8 @@ logger = logging.getLogger(__name__)
|
|
|
57
56
|
DEFAULT_UI_PORT = 7654
|
|
58
57
|
|
|
59
58
|
|
|
60
|
-
def _sink_pac_record() ->
|
|
61
|
-
return
|
|
59
|
+
def _sink_pac_record() -> pacs.PacRecord:
|
|
60
|
+
return pacs.PacRecord(
|
|
62
61
|
coding_format=CodingFormat(CodecID.LC3),
|
|
63
62
|
codec_specific_capabilities=bap.CodecSpecificCapabilities(
|
|
64
63
|
supported_sampling_frequencies=(
|
|
@@ -79,8 +78,8 @@ def _sink_pac_record() -> bap.PacRecord:
|
|
|
79
78
|
)
|
|
80
79
|
|
|
81
80
|
|
|
82
|
-
def _source_pac_record() ->
|
|
83
|
-
return
|
|
81
|
+
def _source_pac_record() -> pacs.PacRecord:
|
|
82
|
+
return pacs.PacRecord(
|
|
84
83
|
coding_format=CodingFormat(CodecID.LC3),
|
|
85
84
|
codec_specific_capabilities=bap.CodecSpecificCapabilities(
|
|
86
85
|
supported_sampling_frequencies=(
|
|
@@ -447,7 +446,7 @@ class Speaker:
|
|
|
447
446
|
)
|
|
448
447
|
|
|
449
448
|
self.device.add_service(
|
|
450
|
-
|
|
449
|
+
pacs.PublishedAudioCapabilitiesService(
|
|
451
450
|
supported_source_context=bap.ContextType(0xFFFF),
|
|
452
451
|
available_source_context=bap.ContextType(0xFFFF),
|
|
453
452
|
supported_sink_context=bap.ContextType(0xFFFF), # All context types
|
|
@@ -461,10 +460,10 @@ class Speaker:
|
|
|
461
460
|
)
|
|
462
461
|
)
|
|
463
462
|
|
|
464
|
-
|
|
463
|
+
ascs_service = ascs.AudioStreamControlService(
|
|
465
464
|
self.device, sink_ase_id=[1], source_ase_id=[2]
|
|
466
465
|
)
|
|
467
|
-
self.device.add_service(
|
|
466
|
+
self.device.add_service(ascs_service)
|
|
468
467
|
|
|
469
468
|
advertising_data = bytes(
|
|
470
469
|
AdvertisingData(
|
|
@@ -479,13 +478,13 @@ class Speaker:
|
|
|
479
478
|
),
|
|
480
479
|
(
|
|
481
480
|
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
|
482
|
-
bytes(
|
|
481
|
+
bytes(pacs.PublishedAudioCapabilitiesService.UUID),
|
|
483
482
|
),
|
|
484
483
|
]
|
|
485
484
|
)
|
|
486
485
|
) + bytes(bap.UnicastServerAdvertisingData())
|
|
487
486
|
|
|
488
|
-
def on_pdu(pdu: HCI_IsoDataPacket, ase:
|
|
487
|
+
def on_pdu(pdu: HCI_IsoDataPacket, ase: ascs.AseStateMachine):
|
|
489
488
|
codec_config = ase.codec_specific_configuration
|
|
490
489
|
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
491
490
|
pcm = decode(
|
|
@@ -495,12 +494,12 @@ class Speaker:
|
|
|
495
494
|
)
|
|
496
495
|
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
|
497
496
|
|
|
498
|
-
def on_ase_state_change(ase:
|
|
499
|
-
if ase.state ==
|
|
497
|
+
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
|
498
|
+
if ase.state == ascs.AseStateMachine.State.STREAMING:
|
|
500
499
|
codec_config = ase.codec_specific_configuration
|
|
501
500
|
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
502
501
|
assert ase.cis_link
|
|
503
|
-
if ase.role ==
|
|
502
|
+
if ase.role == ascs.AudioRole.SOURCE:
|
|
504
503
|
ase.cis_link.abort_on(
|
|
505
504
|
'disconnection',
|
|
506
505
|
lc3_source_task(
|
|
@@ -516,10 +515,10 @@ class Speaker:
|
|
|
516
515
|
)
|
|
517
516
|
else:
|
|
518
517
|
ase.cis_link.sink = functools.partial(on_pdu, ase=ase)
|
|
519
|
-
elif ase.state ==
|
|
518
|
+
elif ase.state == ascs.AseStateMachine.State.CODEC_CONFIGURED:
|
|
520
519
|
codec_config = ase.codec_specific_configuration
|
|
521
520
|
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
522
|
-
if ase.role ==
|
|
521
|
+
if ase.role == ascs.AudioRole.SOURCE:
|
|
523
522
|
setup_encoders(
|
|
524
523
|
codec_config.sampling_frequency.hz,
|
|
525
524
|
codec_config.frame_duration.us,
|
|
@@ -532,7 +531,7 @@ class Speaker:
|
|
|
532
531
|
codec_config.audio_channel_allocation.channel_count,
|
|
533
532
|
)
|
|
534
533
|
|
|
535
|
-
for ase in
|
|
534
|
+
for ase in ascs_service.ase_state_machines.values():
|
|
536
535
|
ase.on('state_change', functools.partial(on_ase_state_change, ase=ase))
|
|
537
536
|
|
|
538
537
|
await self.device.power_on()
|
bumble/apps/pair.py
CHANGED
|
@@ -46,6 +46,12 @@ from bumble.att import (
|
|
|
46
46
|
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
|
47
47
|
ATT_INSUFFICIENT_ENCRYPTION_ERROR,
|
|
48
48
|
)
|
|
49
|
+
from bumble.utils import AsyncRunner
|
|
50
|
+
|
|
51
|
+
# -----------------------------------------------------------------------------
|
|
52
|
+
# Constants
|
|
53
|
+
# -----------------------------------------------------------------------------
|
|
54
|
+
POST_PAIRING_DELAY = 1
|
|
49
55
|
|
|
50
56
|
|
|
51
57
|
# -----------------------------------------------------------------------------
|
|
@@ -235,8 +241,10 @@ def on_connection(connection, request):
|
|
|
235
241
|
|
|
236
242
|
# Listen for pairing events
|
|
237
243
|
connection.on('pairing_start', on_pairing_start)
|
|
238
|
-
connection.on('pairing', lambda keys: on_pairing(connection
|
|
239
|
-
connection.on(
|
|
244
|
+
connection.on('pairing', lambda keys: on_pairing(connection, keys))
|
|
245
|
+
connection.on(
|
|
246
|
+
'pairing_failure', lambda reason: on_pairing_failure(connection, reason)
|
|
247
|
+
)
|
|
240
248
|
|
|
241
249
|
# Listen for encryption changes
|
|
242
250
|
connection.on(
|
|
@@ -270,19 +278,24 @@ def on_pairing_start():
|
|
|
270
278
|
|
|
271
279
|
|
|
272
280
|
# -----------------------------------------------------------------------------
|
|
273
|
-
|
|
281
|
+
@AsyncRunner.run_in_task()
|
|
282
|
+
async def on_pairing(connection, keys):
|
|
274
283
|
print(color('***-----------------------------------', 'cyan'))
|
|
275
|
-
print(color(f'*** Paired! (peer identity={
|
|
284
|
+
print(color(f'*** Paired! (peer identity={connection.peer_address})', 'cyan'))
|
|
276
285
|
keys.print(prefix=color('*** ', 'cyan'))
|
|
277
286
|
print(color('***-----------------------------------', 'cyan'))
|
|
287
|
+
await asyncio.sleep(POST_PAIRING_DELAY)
|
|
288
|
+
await connection.disconnect()
|
|
278
289
|
Waiter.instance.terminate()
|
|
279
290
|
|
|
280
291
|
|
|
281
292
|
# -----------------------------------------------------------------------------
|
|
282
|
-
|
|
293
|
+
@AsyncRunner.run_in_task()
|
|
294
|
+
async def on_pairing_failure(connection, reason):
|
|
283
295
|
print(color('***-----------------------------------', 'red'))
|
|
284
296
|
print(color(f'*** Pairing failed: {smp_error_name(reason)}', 'red'))
|
|
285
297
|
print(color('***-----------------------------------', 'red'))
|
|
298
|
+
await connection.disconnect()
|
|
286
299
|
Waiter.instance.terminate()
|
|
287
300
|
|
|
288
301
|
|
|
@@ -293,6 +306,7 @@ async def pair(
|
|
|
293
306
|
mitm,
|
|
294
307
|
bond,
|
|
295
308
|
ctkd,
|
|
309
|
+
identity_address,
|
|
296
310
|
linger,
|
|
297
311
|
io,
|
|
298
312
|
oob,
|
|
@@ -382,11 +396,18 @@ async def pair(
|
|
|
382
396
|
oob_contexts = None
|
|
383
397
|
|
|
384
398
|
# Set up a pairing config factory
|
|
399
|
+
if identity_address == 'public':
|
|
400
|
+
identity_address_type = PairingConfig.AddressType.PUBLIC
|
|
401
|
+
elif identity_address == 'random':
|
|
402
|
+
identity_address_type = PairingConfig.AddressType.RANDOM
|
|
403
|
+
else:
|
|
404
|
+
identity_address_type = None
|
|
385
405
|
device.pairing_config_factory = lambda connection: PairingConfig(
|
|
386
406
|
sc=sc,
|
|
387
407
|
mitm=mitm,
|
|
388
408
|
bonding=bond,
|
|
389
409
|
oob=oob_contexts,
|
|
410
|
+
identity_address_type=identity_address_type,
|
|
390
411
|
delegate=Delegate(mode, connection, io, prompt),
|
|
391
412
|
)
|
|
392
413
|
|
|
@@ -457,6 +478,10 @@ class LogHandler(logging.Handler):
|
|
|
457
478
|
help='Enable CTKD',
|
|
458
479
|
show_default=True,
|
|
459
480
|
)
|
|
481
|
+
@click.option(
|
|
482
|
+
'--identity-address',
|
|
483
|
+
type=click.Choice(['random', 'public']),
|
|
484
|
+
)
|
|
460
485
|
@click.option('--linger', default=False, is_flag=True, help='Linger after pairing')
|
|
461
486
|
@click.option(
|
|
462
487
|
'--io',
|
|
@@ -493,6 +518,7 @@ def main(
|
|
|
493
518
|
mitm,
|
|
494
519
|
bond,
|
|
495
520
|
ctkd,
|
|
521
|
+
identity_address,
|
|
496
522
|
linger,
|
|
497
523
|
io,
|
|
498
524
|
oob,
|
|
@@ -518,6 +544,7 @@ def main(
|
|
|
518
544
|
mitm,
|
|
519
545
|
bond,
|
|
520
546
|
ctkd,
|
|
547
|
+
identity_address,
|
|
521
548
|
linger,
|
|
522
549
|
io,
|
|
523
550
|
oob,
|
bumble/at.py
CHANGED
|
@@ -14,13 +14,19 @@
|
|
|
14
14
|
|
|
15
15
|
from typing import List, Union
|
|
16
16
|
|
|
17
|
+
from bumble import core
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AtParsingError(core.InvalidPacketError):
|
|
21
|
+
"""Error raised when parsing AT commands fails."""
|
|
22
|
+
|
|
17
23
|
|
|
18
24
|
def tokenize_parameters(buffer: bytes) -> List[bytes]:
|
|
19
25
|
"""Split input parameters into tokens.
|
|
20
26
|
Removes space characters outside of double quote blocks:
|
|
21
27
|
T-rec-V-25 - 5.2.1 Command line general format: "Space characters (IA5 2/0)
|
|
22
28
|
are ignored [..], unless they are embedded in numeric or string constants"
|
|
23
|
-
Raises
|
|
29
|
+
Raises AtParsingError in case of invalid input string."""
|
|
24
30
|
|
|
25
31
|
tokens = []
|
|
26
32
|
in_quotes = False
|
|
@@ -43,11 +49,11 @@ def tokenize_parameters(buffer: bytes) -> List[bytes]:
|
|
|
43
49
|
token = bytearray()
|
|
44
50
|
elif char == b'(':
|
|
45
51
|
if len(token) > 0:
|
|
46
|
-
raise
|
|
52
|
+
raise AtParsingError("open_paren following regular character")
|
|
47
53
|
tokens.append(char)
|
|
48
54
|
elif char == b'"':
|
|
49
55
|
if len(token) > 0:
|
|
50
|
-
raise
|
|
56
|
+
raise AtParsingError("quote following regular character")
|
|
51
57
|
in_quotes = True
|
|
52
58
|
token.extend(char)
|
|
53
59
|
else:
|
|
@@ -59,7 +65,7 @@ def tokenize_parameters(buffer: bytes) -> List[bytes]:
|
|
|
59
65
|
|
|
60
66
|
def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
|
|
61
67
|
"""Parse the parameters using the comma and parenthesis separators.
|
|
62
|
-
Raises
|
|
68
|
+
Raises AtParsingError in case of invalid input string."""
|
|
63
69
|
|
|
64
70
|
tokens = tokenize_parameters(buffer)
|
|
65
71
|
accumulator: List[list] = [[]]
|
|
@@ -73,7 +79,7 @@ def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
|
|
|
73
79
|
accumulator.append([])
|
|
74
80
|
elif token == b')':
|
|
75
81
|
if len(accumulator) < 2:
|
|
76
|
-
raise
|
|
82
|
+
raise AtParsingError("close_paren without matching open_paren")
|
|
77
83
|
accumulator[-1].append(current)
|
|
78
84
|
current = accumulator.pop()
|
|
79
85
|
else:
|
|
@@ -81,5 +87,5 @@ def parse_parameters(buffer: bytes) -> List[Union[bytes, list]]:
|
|
|
81
87
|
|
|
82
88
|
accumulator[-1].append(current)
|
|
83
89
|
if len(accumulator) > 1:
|
|
84
|
-
raise
|
|
90
|
+
raise AtParsingError("missing close_paren")
|
|
85
91
|
return accumulator[0]
|
bumble/att.py
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
# Imports
|
|
24
24
|
# -----------------------------------------------------------------------------
|
|
25
25
|
from __future__ import annotations
|
|
26
|
+
|
|
26
27
|
import enum
|
|
27
28
|
import functools
|
|
28
29
|
import inspect
|
|
@@ -41,6 +42,7 @@ from typing import (
|
|
|
41
42
|
|
|
42
43
|
from pyee import EventEmitter
|
|
43
44
|
|
|
45
|
+
from bumble import utils
|
|
44
46
|
from bumble.core import UUID, name_or_number, ProtocolError
|
|
45
47
|
from bumble.hci import HCI_Object, key_with_value
|
|
46
48
|
from bumble.colors import color
|
|
@@ -145,43 +147,57 @@ ATT_RESPONSES = [
|
|
|
145
147
|
ATT_EXECUTE_WRITE_RESPONSE
|
|
146
148
|
]
|
|
147
149
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
150
|
+
class ErrorCode(utils.OpenIntEnum):
|
|
151
|
+
'''
|
|
152
|
+
See
|
|
153
|
+
|
|
154
|
+
* Bluetooth spec @ Vol 3, Part F - 3.4.1.1 Error Response
|
|
155
|
+
* Core Specification Supplement: Common Profile And Service Error Codes
|
|
156
|
+
'''
|
|
157
|
+
INVALID_HANDLE = 0x01
|
|
158
|
+
READ_NOT_PERMITTED = 0x02
|
|
159
|
+
WRITE_NOT_PERMITTED = 0x03
|
|
160
|
+
INVALID_PDU = 0x04
|
|
161
|
+
INSUFFICIENT_AUTHENTICATION = 0x05
|
|
162
|
+
REQUEST_NOT_SUPPORTED = 0x06
|
|
163
|
+
INVALID_OFFSET = 0x07
|
|
164
|
+
INSUFFICIENT_AUTHORIZATION = 0x08
|
|
165
|
+
PREPARE_QUEUE_FULL = 0x09
|
|
166
|
+
ATTRIBUTE_NOT_FOUND = 0x0A
|
|
167
|
+
ATTRIBUTE_NOT_LONG = 0x0B
|
|
168
|
+
INSUFFICIENT_ENCRYPTION_KEY_SIZE = 0x0C
|
|
169
|
+
INVALID_ATTRIBUTE_LENGTH = 0x0D
|
|
170
|
+
UNLIKELY_ERROR = 0x0E
|
|
171
|
+
INSUFFICIENT_ENCRYPTION = 0x0F
|
|
172
|
+
UNSUPPORTED_GROUP_TYPE = 0x10
|
|
173
|
+
INSUFFICIENT_RESOURCES = 0x11
|
|
174
|
+
DATABASE_OUT_OF_SYNC = 0x12
|
|
175
|
+
VALUE_NOT_ALLOWED = 0x13
|
|
176
|
+
# 0x80 – 0x9F: Application Error
|
|
177
|
+
# 0xE0 – 0xFF: Common Profile and Service Error Codes
|
|
178
|
+
WRITE_REQUEST_REJECTED = 0xFC
|
|
179
|
+
CCCD_IMPROPERLY_CONFIGURED = 0xFD
|
|
180
|
+
PROCEDURE_ALREADY_IN_PROGRESS = 0xFE
|
|
181
|
+
OUT_OF_RANGE = 0xFF
|
|
182
|
+
|
|
183
|
+
# Backward Compatible Constants
|
|
184
|
+
ATT_INVALID_HANDLE_ERROR = ErrorCode.INVALID_HANDLE
|
|
185
|
+
ATT_READ_NOT_PERMITTED_ERROR = ErrorCode.READ_NOT_PERMITTED
|
|
186
|
+
ATT_WRITE_NOT_PERMITTED_ERROR = ErrorCode.WRITE_NOT_PERMITTED
|
|
187
|
+
ATT_INVALID_PDU_ERROR = ErrorCode.INVALID_PDU
|
|
188
|
+
ATT_INSUFFICIENT_AUTHENTICATION_ERROR = ErrorCode.INSUFFICIENT_AUTHENTICATION
|
|
189
|
+
ATT_REQUEST_NOT_SUPPORTED_ERROR = ErrorCode.REQUEST_NOT_SUPPORTED
|
|
190
|
+
ATT_INVALID_OFFSET_ERROR = ErrorCode.INVALID_OFFSET
|
|
191
|
+
ATT_INSUFFICIENT_AUTHORIZATION_ERROR = ErrorCode.INSUFFICIENT_AUTHORIZATION
|
|
192
|
+
ATT_PREPARE_QUEUE_FULL_ERROR = ErrorCode.PREPARE_QUEUE_FULL
|
|
193
|
+
ATT_ATTRIBUTE_NOT_FOUND_ERROR = ErrorCode.ATTRIBUTE_NOT_FOUND
|
|
194
|
+
ATT_ATTRIBUTE_NOT_LONG_ERROR = ErrorCode.ATTRIBUTE_NOT_LONG
|
|
195
|
+
ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR = ErrorCode.INSUFFICIENT_ENCRYPTION_KEY_SIZE
|
|
196
|
+
ATT_INVALID_ATTRIBUTE_LENGTH_ERROR = ErrorCode.INVALID_ATTRIBUTE_LENGTH
|
|
197
|
+
ATT_UNLIKELY_ERROR_ERROR = ErrorCode.UNLIKELY_ERROR
|
|
198
|
+
ATT_INSUFFICIENT_ENCRYPTION_ERROR = ErrorCode.INSUFFICIENT_ENCRYPTION
|
|
199
|
+
ATT_UNSUPPORTED_GROUP_TYPE_ERROR = ErrorCode.UNSUPPORTED_GROUP_TYPE
|
|
200
|
+
ATT_INSUFFICIENT_RESOURCES_ERROR = ErrorCode.INSUFFICIENT_RESOURCES
|
|
185
201
|
|
|
186
202
|
ATT_DEFAULT_MTU = 23
|
|
187
203
|
|
|
@@ -245,9 +261,9 @@ class ATT_PDU:
|
|
|
245
261
|
def pdu_name(op_code):
|
|
246
262
|
return name_or_number(ATT_PDU_NAMES, op_code, 2)
|
|
247
263
|
|
|
248
|
-
@
|
|
249
|
-
def error_name(error_code):
|
|
250
|
-
return
|
|
264
|
+
@classmethod
|
|
265
|
+
def error_name(cls, error_code: int) -> str:
|
|
266
|
+
return ErrorCode(error_code).name
|
|
251
267
|
|
|
252
268
|
@staticmethod
|
|
253
269
|
def subclass(fields):
|