bumble 0.0.194__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 +692 -0
- bumble/apps/bench.py +77 -23
- bumble/apps/console.py +5 -20
- bumble/apps/controller_info.py +3 -3
- 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 +726 -122
- bumble/device.py +817 -117
- bumble/drivers/rtk.py +13 -8
- bumble/gatt.py +6 -1
- bumble/gatt_client.py +10 -4
- bumble/hci.py +283 -20
- bumble/hid.py +24 -28
- bumble/host.py +29 -0
- bumble/l2cap.py +24 -17
- bumble/link.py +8 -3
- bumble/pandora/host.py +3 -2
- bumble/profiles/ascs.py +739 -0
- bumble/profiles/bap.py +85 -862
- 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 +83 -0
- bumble/profiles/mcp.py +448 -0
- bumble/profiles/pacs.py +210 -0
- bumble/profiles/pbp.py +46 -0
- bumble/profiles/tmap.py +89 -0
- bumble/rfcomm.py +14 -3
- 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.194.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/RECORD +54 -43
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
bumble/apps/bench.py
CHANGED
|
@@ -40,6 +40,8 @@ from bumble.hci import (
|
|
|
40
40
|
HCI_LE_1M_PHY,
|
|
41
41
|
HCI_LE_2M_PHY,
|
|
42
42
|
HCI_LE_CODED_PHY,
|
|
43
|
+
HCI_CENTRAL_ROLE,
|
|
44
|
+
HCI_PERIPHERAL_ROLE,
|
|
43
45
|
HCI_Constant,
|
|
44
46
|
HCI_Error,
|
|
45
47
|
HCI_StatusError,
|
|
@@ -57,6 +59,7 @@ from bumble.transport import open_transport_or_link
|
|
|
57
59
|
import bumble.rfcomm
|
|
58
60
|
import bumble.core
|
|
59
61
|
from bumble.utils import AsyncRunner
|
|
62
|
+
from bumble.pairing import PairingConfig
|
|
60
63
|
|
|
61
64
|
|
|
62
65
|
# -----------------------------------------------------------------------------
|
|
@@ -128,40 +131,34 @@ def le_phy_name(phy_id):
|
|
|
128
131
|
|
|
129
132
|
|
|
130
133
|
def print_connection(connection):
|
|
134
|
+
params = []
|
|
131
135
|
if connection.transport == BT_LE_TRANSPORT:
|
|
132
|
-
|
|
136
|
+
params.append(
|
|
133
137
|
'PHY='
|
|
134
138
|
f'TX:{le_phy_name(connection.phy.tx_phy)}/'
|
|
135
139
|
f'RX:{le_phy_name(connection.phy.rx_phy)}'
|
|
136
140
|
)
|
|
137
141
|
|
|
138
|
-
|
|
142
|
+
params.append(
|
|
139
143
|
'DL=('
|
|
140
144
|
f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
|
|
141
145
|
f'RX:{connection.data_length[2]}/{connection.data_length[3]}'
|
|
142
146
|
')'
|
|
143
147
|
)
|
|
144
|
-
|
|
148
|
+
|
|
149
|
+
params.append(
|
|
145
150
|
'Parameters='
|
|
146
151
|
f'{connection.parameters.connection_interval * 1.25:.2f}/'
|
|
147
152
|
f'{connection.parameters.peripheral_latency}/'
|
|
148
153
|
f'{connection.parameters.supervision_timeout * 10} '
|
|
149
154
|
)
|
|
150
155
|
|
|
151
|
-
|
|
152
|
-
phy_state = ''
|
|
153
|
-
data_length = ''
|
|
154
|
-
connection_parameters = ''
|
|
156
|
+
params.append(f'MTU={connection.att_mtu}')
|
|
155
157
|
|
|
156
|
-
|
|
158
|
+
else:
|
|
159
|
+
params.append(f'Role={HCI_Constant.role_name(connection.role)}')
|
|
157
160
|
|
|
158
|
-
logging.info(
|
|
159
|
-
f'{color("@@@ Connection:", "yellow")} '
|
|
160
|
-
f'{connection_parameters} '
|
|
161
|
-
f'{data_length} '
|
|
162
|
-
f'{phy_state} '
|
|
163
|
-
f'MTU={mtu}'
|
|
164
|
-
)
|
|
161
|
+
logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params))
|
|
165
162
|
|
|
166
163
|
|
|
167
164
|
def make_sdp_records(channel):
|
|
@@ -214,6 +211,17 @@ def log_stats(title, stats):
|
|
|
214
211
|
)
|
|
215
212
|
|
|
216
213
|
|
|
214
|
+
async def switch_roles(connection, role):
|
|
215
|
+
target_role = HCI_CENTRAL_ROLE if role == "central" else HCI_PERIPHERAL_ROLE
|
|
216
|
+
if connection.role != target_role:
|
|
217
|
+
logging.info(f'{color("### Switching roles to:", "cyan")} {role}')
|
|
218
|
+
try:
|
|
219
|
+
await connection.switch_role(target_role)
|
|
220
|
+
logging.info(color('### Role switch complete', 'cyan'))
|
|
221
|
+
except HCI_Error as error:
|
|
222
|
+
logging.info(f'{color("### Role switch failed:", "red")} {error}')
|
|
223
|
+
|
|
224
|
+
|
|
217
225
|
class PacketType(enum.IntEnum):
|
|
218
226
|
RESET = 0
|
|
219
227
|
SEQUENCE = 1
|
|
@@ -1034,6 +1042,10 @@ class RfcommServer(StreamedPacketIO):
|
|
|
1034
1042
|
|
|
1035
1043
|
def on_dlc(self, dlc):
|
|
1036
1044
|
logging.info(color(f'*** DLC connected: {dlc}', 'blue'))
|
|
1045
|
+
if self.credits_threshold is not None:
|
|
1046
|
+
dlc.rx_threshold = self.credits_threshold
|
|
1047
|
+
if self.max_credits is not None:
|
|
1048
|
+
dlc.rx_max_credits = self.max_credits
|
|
1037
1049
|
dlc.sink = self.on_packet
|
|
1038
1050
|
self.io_sink = dlc.write
|
|
1039
1051
|
self.dlc = dlc
|
|
@@ -1063,6 +1075,7 @@ class Central(Connection.Listener):
|
|
|
1063
1075
|
authenticate,
|
|
1064
1076
|
encrypt,
|
|
1065
1077
|
extended_data_length,
|
|
1078
|
+
role_switch,
|
|
1066
1079
|
):
|
|
1067
1080
|
super().__init__()
|
|
1068
1081
|
self.transport = transport
|
|
@@ -1073,6 +1086,7 @@ class Central(Connection.Listener):
|
|
|
1073
1086
|
self.authenticate = authenticate
|
|
1074
1087
|
self.encrypt = encrypt or authenticate
|
|
1075
1088
|
self.extended_data_length = extended_data_length
|
|
1089
|
+
self.role_switch = role_switch
|
|
1076
1090
|
self.device = None
|
|
1077
1091
|
self.connection = None
|
|
1078
1092
|
|
|
@@ -1123,6 +1137,11 @@ class Central(Connection.Listener):
|
|
|
1123
1137
|
role = self.role_factory(mode)
|
|
1124
1138
|
self.device.classic_enabled = self.classic
|
|
1125
1139
|
|
|
1140
|
+
# Set up a pairing config factory with minimal requirements.
|
|
1141
|
+
self.device.pairing_config_factory = lambda _: PairingConfig(
|
|
1142
|
+
sc=False, mitm=False, bonding=False
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1126
1145
|
await self.device.power_on()
|
|
1127
1146
|
|
|
1128
1147
|
if self.classic:
|
|
@@ -1151,6 +1170,10 @@ class Central(Connection.Listener):
|
|
|
1151
1170
|
self.connection.listener = self
|
|
1152
1171
|
print_connection(self.connection)
|
|
1153
1172
|
|
|
1173
|
+
# Switch roles if needed.
|
|
1174
|
+
if self.role_switch:
|
|
1175
|
+
await switch_roles(self.connection, self.role_switch)
|
|
1176
|
+
|
|
1154
1177
|
# Wait a bit after the connection, some controllers aren't very good when
|
|
1155
1178
|
# we start sending data right away while some connection parameters are
|
|
1156
1179
|
# updated post connection
|
|
@@ -1212,20 +1235,30 @@ class Central(Connection.Listener):
|
|
|
1212
1235
|
def on_connection_data_length_change(self):
|
|
1213
1236
|
print_connection(self.connection)
|
|
1214
1237
|
|
|
1238
|
+
def on_role_change(self):
|
|
1239
|
+
print_connection(self.connection)
|
|
1240
|
+
|
|
1215
1241
|
|
|
1216
1242
|
# -----------------------------------------------------------------------------
|
|
1217
1243
|
# Peripheral
|
|
1218
1244
|
# -----------------------------------------------------------------------------
|
|
1219
1245
|
class Peripheral(Device.Listener, Connection.Listener):
|
|
1220
1246
|
def __init__(
|
|
1221
|
-
self,
|
|
1247
|
+
self,
|
|
1248
|
+
transport,
|
|
1249
|
+
role_factory,
|
|
1250
|
+
mode_factory,
|
|
1251
|
+
classic,
|
|
1252
|
+
extended_data_length,
|
|
1253
|
+
role_switch,
|
|
1222
1254
|
):
|
|
1223
1255
|
self.transport = transport
|
|
1224
1256
|
self.classic = classic
|
|
1225
|
-
self.extended_data_length = extended_data_length
|
|
1226
1257
|
self.role_factory = role_factory
|
|
1227
|
-
self.role = None
|
|
1228
1258
|
self.mode_factory = mode_factory
|
|
1259
|
+
self.extended_data_length = extended_data_length
|
|
1260
|
+
self.role_switch = role_switch
|
|
1261
|
+
self.role = None
|
|
1229
1262
|
self.mode = None
|
|
1230
1263
|
self.device = None
|
|
1231
1264
|
self.connection = None
|
|
@@ -1248,6 +1281,11 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1248
1281
|
self.role = self.role_factory(self.mode)
|
|
1249
1282
|
self.device.classic_enabled = self.classic
|
|
1250
1283
|
|
|
1284
|
+
# Set up a pairing config factory with minimal requirements.
|
|
1285
|
+
self.device.pairing_config_factory = lambda _: PairingConfig(
|
|
1286
|
+
sc=False, mitm=False, bonding=False
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1251
1289
|
await self.device.power_on()
|
|
1252
1290
|
|
|
1253
1291
|
if self.classic:
|
|
@@ -1274,6 +1312,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1274
1312
|
|
|
1275
1313
|
await self.connected.wait()
|
|
1276
1314
|
logging.info(color('### Connected', 'cyan'))
|
|
1315
|
+
print_connection(self.connection)
|
|
1277
1316
|
|
|
1278
1317
|
await self.mode.on_connection(self.connection)
|
|
1279
1318
|
await self.role.run()
|
|
@@ -1290,7 +1329,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1290
1329
|
AsyncRunner.spawn(self.device.set_connectable(False))
|
|
1291
1330
|
|
|
1292
1331
|
# Request a new data length if needed
|
|
1293
|
-
if self.extended_data_length:
|
|
1332
|
+
if not self.classic and self.extended_data_length:
|
|
1294
1333
|
logging.info("+++ Requesting extended data length")
|
|
1295
1334
|
AsyncRunner.spawn(
|
|
1296
1335
|
connection.set_data_length(
|
|
@@ -1298,6 +1337,10 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1298
1337
|
)
|
|
1299
1338
|
)
|
|
1300
1339
|
|
|
1340
|
+
# Switch roles if needed.
|
|
1341
|
+
if self.role_switch:
|
|
1342
|
+
AsyncRunner.spawn(switch_roles(connection, self.role_switch))
|
|
1343
|
+
|
|
1301
1344
|
def on_disconnection(self, reason):
|
|
1302
1345
|
logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
|
|
1303
1346
|
self.connection = None
|
|
@@ -1319,6 +1362,9 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1319
1362
|
def on_connection_data_length_change(self):
|
|
1320
1363
|
print_connection(self.connection)
|
|
1321
1364
|
|
|
1365
|
+
def on_role_change(self):
|
|
1366
|
+
print_connection(self.connection)
|
|
1367
|
+
|
|
1322
1368
|
|
|
1323
1369
|
# -----------------------------------------------------------------------------
|
|
1324
1370
|
def create_mode_factory(ctx, default_mode):
|
|
@@ -1448,6 +1494,11 @@ def create_role_factory(ctx, default_role):
|
|
|
1448
1494
|
'--extended-data-length',
|
|
1449
1495
|
help='Request a data length upon connection, specified as tx_octets/tx_time',
|
|
1450
1496
|
)
|
|
1497
|
+
@click.option(
|
|
1498
|
+
'--role-switch',
|
|
1499
|
+
type=click.Choice(['central', 'peripheral']),
|
|
1500
|
+
help='Request role switch upon connection (central or peripheral)',
|
|
1501
|
+
)
|
|
1451
1502
|
@click.option(
|
|
1452
1503
|
'--rfcomm-channel',
|
|
1453
1504
|
type=int,
|
|
@@ -1512,7 +1563,7 @@ def create_role_factory(ctx, default_role):
|
|
|
1512
1563
|
'--packet-size',
|
|
1513
1564
|
'-s',
|
|
1514
1565
|
metavar='SIZE',
|
|
1515
|
-
type=click.IntRange(8,
|
|
1566
|
+
type=click.IntRange(8, 8192),
|
|
1516
1567
|
default=500,
|
|
1517
1568
|
help='Packet size (client or ping role)',
|
|
1518
1569
|
)
|
|
@@ -1572,6 +1623,7 @@ def bench(
|
|
|
1572
1623
|
mode,
|
|
1573
1624
|
att_mtu,
|
|
1574
1625
|
extended_data_length,
|
|
1626
|
+
role_switch,
|
|
1575
1627
|
packet_size,
|
|
1576
1628
|
packet_count,
|
|
1577
1629
|
start_delay,
|
|
@@ -1614,12 +1666,12 @@ def bench(
|
|
|
1614
1666
|
ctx.obj['repeat_delay'] = repeat_delay
|
|
1615
1667
|
ctx.obj['pace'] = pace
|
|
1616
1668
|
ctx.obj['linger'] = linger
|
|
1617
|
-
|
|
1618
1669
|
ctx.obj['extended_data_length'] = (
|
|
1619
1670
|
[int(x) for x in extended_data_length.split('/')]
|
|
1620
1671
|
if extended_data_length
|
|
1621
1672
|
else None
|
|
1622
1673
|
)
|
|
1674
|
+
ctx.obj['role_switch'] = role_switch
|
|
1623
1675
|
ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
|
|
1624
1676
|
|
|
1625
1677
|
|
|
@@ -1663,6 +1715,7 @@ def central(
|
|
|
1663
1715
|
authenticate,
|
|
1664
1716
|
encrypt or authenticate,
|
|
1665
1717
|
ctx.obj['extended_data_length'],
|
|
1718
|
+
ctx.obj['role_switch'],
|
|
1666
1719
|
).run()
|
|
1667
1720
|
|
|
1668
1721
|
asyncio.run(run_central())
|
|
@@ -1679,10 +1732,11 @@ def peripheral(ctx, transport):
|
|
|
1679
1732
|
async def run_peripheral():
|
|
1680
1733
|
await Peripheral(
|
|
1681
1734
|
transport,
|
|
1682
|
-
ctx.obj['classic'],
|
|
1683
|
-
ctx.obj['extended_data_length'],
|
|
1684
1735
|
role_factory,
|
|
1685
1736
|
mode_factory,
|
|
1737
|
+
ctx.obj['classic'],
|
|
1738
|
+
ctx.obj['extended_data_length'],
|
|
1739
|
+
ctx.obj['role_switch'],
|
|
1686
1740
|
).run()
|
|
1687
1741
|
|
|
1688
1742
|
asyncio.run(run_peripheral())
|
bumble/apps/console.py
CHANGED
|
@@ -63,6 +63,7 @@ from bumble.transport import open_transport_or_link
|
|
|
63
63
|
from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
|
|
64
64
|
from bumble.gatt_client import CharacteristicProxy
|
|
65
65
|
from bumble.hci import (
|
|
66
|
+
Address,
|
|
66
67
|
HCI_Constant,
|
|
67
68
|
HCI_LE_1M_PHY,
|
|
68
69
|
HCI_LE_2M_PHY,
|
|
@@ -289,11 +290,7 @@ class ConsoleApp:
|
|
|
289
290
|
device_config, hci_source, hci_sink
|
|
290
291
|
)
|
|
291
292
|
else:
|
|
292
|
-
random_address = (
|
|
293
|
-
f"{random.randint(192,255):02X}" # address is static random
|
|
294
|
-
)
|
|
295
|
-
for random_byte in random.sample(range(255), 5):
|
|
296
|
-
random_address += f":{random_byte:02X}"
|
|
293
|
+
random_address = Address.generate_static_address()
|
|
297
294
|
self.append_to_log(f"Setting random address: {random_address}")
|
|
298
295
|
self.device = Device.with_hci(
|
|
299
296
|
'Bumble', random_address, hci_source, hci_sink
|
|
@@ -503,21 +500,9 @@ class ConsoleApp:
|
|
|
503
500
|
self.show_error('not connected')
|
|
504
501
|
return
|
|
505
502
|
|
|
506
|
-
|
|
507
|
-
self.
|
|
508
|
-
|
|
509
|
-
self.append_to_output(
|
|
510
|
-
f'found {len(self.connected_peer.services)} services,'
|
|
511
|
-
' discovering characteristics...'
|
|
512
|
-
)
|
|
513
|
-
await self.connected_peer.discover_characteristics()
|
|
514
|
-
self.append_to_output('found characteristics, discovering descriptors...')
|
|
515
|
-
for service in self.connected_peer.services:
|
|
516
|
-
for characteristic in service.characteristics:
|
|
517
|
-
await self.connected_peer.discover_descriptors(characteristic)
|
|
518
|
-
self.append_to_output('discovery completed')
|
|
519
|
-
|
|
520
|
-
self.show_remote_services(self.connected_peer.services)
|
|
503
|
+
self.append_to_output('Service Discovery starting...')
|
|
504
|
+
await self.connected_peer.discover_all()
|
|
505
|
+
self.append_to_output('Service Discovery done!')
|
|
521
506
|
|
|
522
507
|
async def discover_attributes(self):
|
|
523
508
|
if not self.connected_peer:
|
bumble/apps/controller_info.py
CHANGED
|
@@ -27,7 +27,7 @@ from bumble.colors import color
|
|
|
27
27
|
from bumble.core import name_or_number
|
|
28
28
|
from bumble.hci import (
|
|
29
29
|
map_null_terminated_utf8_string,
|
|
30
|
-
|
|
30
|
+
LeFeature,
|
|
31
31
|
HCI_SUCCESS,
|
|
32
32
|
HCI_VERSION_NAMES,
|
|
33
33
|
LMP_VERSION_NAMES,
|
|
@@ -140,7 +140,7 @@ async def get_le_info(host: Host) -> None:
|
|
|
140
140
|
|
|
141
141
|
print(color('LE Features:', 'yellow'))
|
|
142
142
|
for feature in host.supported_le_features:
|
|
143
|
-
print(
|
|
143
|
+
print(f' {LeFeature(feature).name}')
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
# -----------------------------------------------------------------------------
|
|
@@ -224,7 +224,7 @@ async def async_main(latency_probes, transport):
|
|
|
224
224
|
print()
|
|
225
225
|
print(color('Supported Commands:', 'yellow'))
|
|
226
226
|
for command in host.supported_commands:
|
|
227
|
-
print('
|
|
227
|
+
print(f' {HCI_Command.command_name(command)}')
|
|
228
228
|
|
|
229
229
|
|
|
230
230
|
# -----------------------------------------------------------------------------
|
|
@@ -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:
|