bumble 0.0.201__py3-none-any.whl → 0.0.203__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 +22 -13
- bumble/apps/bench.py +138 -93
- bumble/apps/hci_bridge.py +1 -1
- bumble/apps/lea_unicast/app.py +24 -6
- bumble/apps/rfcomm_bridge.py +10 -1
- bumble/att.py +1 -4
- bumble/avc.py +2 -0
- bumble/controller.py +58 -2
- bumble/device.py +454 -494
- bumble/gatt.py +1 -1
- bumble/gatt_client.py +9 -3
- bumble/gatt_server.py +2 -2
- bumble/hci.py +93 -33
- bumble/hfp.py +20 -17
- bumble/host.py +1 -1
- bumble/l2cap.py +3 -8
- bumble/link.py +2 -0
- bumble/pandora/host.py +1 -1
- bumble/profiles/aics.py +3 -3
- bumble/profiles/bap.py +116 -41
- bumble/sdp.py +3 -7
- bumble/smp.py +3 -6
- bumble/transport/common.py +4 -2
- {bumble-0.0.201.dist-info → bumble-0.0.203.dist-info}/METADATA +18 -17
- {bumble-0.0.201.dist-info → bumble-0.0.203.dist-info}/RECORD +30 -30
- {bumble-0.0.201.dist-info → bumble-0.0.203.dist-info}/WHEEL +1 -1
- {bumble-0.0.201.dist-info → bumble-0.0.203.dist-info}/LICENSE +0 -0
- {bumble-0.0.201.dist-info → bumble-0.0.203.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.201.dist-info → bumble-0.0.203.dist-info}/top_level.txt +0 -0
bumble/_version.py
CHANGED
bumble/apps/auracast.py
CHANGED
|
@@ -60,7 +60,7 @@ AURACAST_DEFAULT_ATT_MTU = 256
|
|
|
60
60
|
class BroadcastScanner(pyee.EventEmitter):
|
|
61
61
|
@dataclasses.dataclass
|
|
62
62
|
class Broadcast(pyee.EventEmitter):
|
|
63
|
-
name: str
|
|
63
|
+
name: str | None
|
|
64
64
|
sync: bumble.device.PeriodicAdvertisingSync
|
|
65
65
|
rssi: int = 0
|
|
66
66
|
public_broadcast_announcement: Optional[
|
|
@@ -135,7 +135,8 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
135
135
|
self.sync.advertiser_address,
|
|
136
136
|
color(self.sync.state.name, 'green'),
|
|
137
137
|
)
|
|
138
|
-
|
|
138
|
+
if self.name is not None:
|
|
139
|
+
print(f' {color("Name", "cyan")}: {self.name}')
|
|
139
140
|
if self.appearance:
|
|
140
141
|
print(f' {color("Appearance", "cyan")}: {str(self.appearance)}')
|
|
141
142
|
print(f' {color("RSSI", "cyan")}: {self.rssi}')
|
|
@@ -174,7 +175,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
174
175
|
print(color(' Codec ID:', 'yellow'))
|
|
175
176
|
print(
|
|
176
177
|
color(' Coding Format: ', 'green'),
|
|
177
|
-
subgroup.codec_id.
|
|
178
|
+
subgroup.codec_id.codec_id.name,
|
|
178
179
|
)
|
|
179
180
|
print(
|
|
180
181
|
color(' Company ID: ', 'green'),
|
|
@@ -274,13 +275,24 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
274
275
|
await self.device.stop_scanning()
|
|
275
276
|
|
|
276
277
|
def on_advertisement(self, advertisement: bumble.device.Advertisement) -> None:
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
bumble.core.AdvertisingData.
|
|
278
|
+
if not (
|
|
279
|
+
ads := advertisement.data.get_all(
|
|
280
|
+
bumble.core.AdvertisingData.SERVICE_DATA_16_BIT_UUID
|
|
280
281
|
)
|
|
281
|
-
)
|
|
282
|
+
) or not (
|
|
283
|
+
any(
|
|
284
|
+
ad
|
|
285
|
+
for ad in ads
|
|
286
|
+
if isinstance(ad, tuple)
|
|
287
|
+
and ad[0] == bumble.gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE
|
|
288
|
+
)
|
|
289
|
+
):
|
|
282
290
|
return
|
|
283
|
-
|
|
291
|
+
|
|
292
|
+
broadcast_name = advertisement.data.get(
|
|
293
|
+
bumble.core.AdvertisingData.BROADCAST_NAME
|
|
294
|
+
)
|
|
295
|
+
assert isinstance(broadcast_name, str) or broadcast_name is None
|
|
284
296
|
|
|
285
297
|
if broadcast := self.broadcasts.get(advertisement.address):
|
|
286
298
|
broadcast.update(advertisement)
|
|
@@ -291,7 +303,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
291
303
|
)
|
|
292
304
|
|
|
293
305
|
async def on_new_broadcast(
|
|
294
|
-
self, name: str, advertisement: bumble.device.Advertisement
|
|
306
|
+
self, name: str | None, advertisement: bumble.device.Advertisement
|
|
295
307
|
) -> None:
|
|
296
308
|
periodic_advertising_sync = await self.device.create_periodic_advertising_sync(
|
|
297
309
|
advertiser_address=advertisement.address,
|
|
@@ -299,10 +311,7 @@ class BroadcastScanner(pyee.EventEmitter):
|
|
|
299
311
|
sync_timeout=self.sync_timeout,
|
|
300
312
|
filter_duplicates=self.filter_duplicates,
|
|
301
313
|
)
|
|
302
|
-
broadcast = self.Broadcast(
|
|
303
|
-
name,
|
|
304
|
-
periodic_advertising_sync,
|
|
305
|
-
)
|
|
314
|
+
broadcast = self.Broadcast(name, periodic_advertising_sync)
|
|
306
315
|
broadcast.update(advertisement)
|
|
307
316
|
self.broadcasts[advertisement.address] = broadcast
|
|
308
317
|
periodic_advertising_sync.on('loss', lambda: self.on_broadcast_loss(broadcast))
|
bumble/apps/bench.py
CHANGED
|
@@ -19,6 +19,7 @@ import asyncio
|
|
|
19
19
|
import enum
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
|
+
import statistics
|
|
22
23
|
import struct
|
|
23
24
|
import time
|
|
24
25
|
|
|
@@ -194,17 +195,19 @@ def make_sdp_records(channel):
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
|
|
197
|
-
def log_stats(title, stats):
|
|
198
|
+
def log_stats(title, stats, precision=2):
|
|
198
199
|
stats_min = min(stats)
|
|
199
200
|
stats_max = max(stats)
|
|
200
|
-
stats_avg =
|
|
201
|
+
stats_avg = statistics.mean(stats)
|
|
202
|
+
stats_stdev = statistics.stdev(stats)
|
|
201
203
|
logging.info(
|
|
202
204
|
color(
|
|
203
205
|
(
|
|
204
206
|
f'### {title} stats: '
|
|
205
|
-
f'min={stats_min:.
|
|
206
|
-
f'max={stats_max:.
|
|
207
|
-
f'average={stats_avg:.
|
|
207
|
+
f'min={stats_min:.{precision}f}, '
|
|
208
|
+
f'max={stats_max:.{precision}f}, '
|
|
209
|
+
f'average={stats_avg:.{precision}f}, '
|
|
210
|
+
f'stdev={stats_stdev:.{precision}f}'
|
|
208
211
|
),
|
|
209
212
|
'cyan',
|
|
210
213
|
)
|
|
@@ -448,9 +451,9 @@ class Ping:
|
|
|
448
451
|
self.repeat_delay = repeat_delay
|
|
449
452
|
self.pace = pace
|
|
450
453
|
self.done = asyncio.Event()
|
|
451
|
-
self.
|
|
452
|
-
self.
|
|
453
|
-
self.
|
|
454
|
+
self.ping_times = []
|
|
455
|
+
self.rtts = []
|
|
456
|
+
self.next_expected_packet_index = 0
|
|
454
457
|
self.min_stats = []
|
|
455
458
|
self.max_stats = []
|
|
456
459
|
self.avg_stats = []
|
|
@@ -477,60 +480,57 @@ class Ping:
|
|
|
477
480
|
logging.info(color('=== Sending RESET', 'magenta'))
|
|
478
481
|
await self.packet_io.send_packet(bytes([PacketType.RESET]))
|
|
479
482
|
|
|
480
|
-
self.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
+
packet_interval = self.pace / 1000
|
|
484
|
+
start_time = time.time()
|
|
485
|
+
self.next_expected_packet_index = 0
|
|
486
|
+
for i in range(self.tx_packet_count):
|
|
487
|
+
target_time = start_time + (i * packet_interval)
|
|
488
|
+
now = time.time()
|
|
489
|
+
if now < target_time:
|
|
490
|
+
await asyncio.sleep(target_time - now)
|
|
491
|
+
|
|
492
|
+
packet = struct.pack(
|
|
493
|
+
'>bbI',
|
|
494
|
+
PacketType.SEQUENCE,
|
|
495
|
+
(PACKET_FLAG_LAST if i == self.tx_packet_count - 1 else 0),
|
|
496
|
+
i,
|
|
497
|
+
) + bytes(self.tx_packet_size - 6)
|
|
498
|
+
logging.info(color(f'Sending packet {i}', 'yellow'))
|
|
499
|
+
self.ping_times.append(time.time())
|
|
500
|
+
await self.packet_io.send_packet(packet)
|
|
483
501
|
|
|
484
502
|
await self.done.wait()
|
|
485
503
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
504
|
+
min_rtt = min(self.rtts)
|
|
505
|
+
max_rtt = max(self.rtts)
|
|
506
|
+
avg_rtt = statistics.mean(self.rtts)
|
|
507
|
+
stdev_rtt = statistics.stdev(self.rtts)
|
|
489
508
|
logging.info(
|
|
490
509
|
color(
|
|
491
|
-
'@@@
|
|
492
|
-
f'min={
|
|
493
|
-
f'max={
|
|
494
|
-
f'average={
|
|
510
|
+
'@@@ RTTs: '
|
|
511
|
+
f'min={min_rtt:.2f}, '
|
|
512
|
+
f'max={max_rtt:.2f}, '
|
|
513
|
+
f'average={avg_rtt:.2f}, '
|
|
514
|
+
f'stdev={stdev_rtt:.2f}'
|
|
495
515
|
)
|
|
496
516
|
)
|
|
497
517
|
|
|
498
|
-
self.min_stats.append(
|
|
499
|
-
self.max_stats.append(
|
|
500
|
-
self.avg_stats.append(
|
|
518
|
+
self.min_stats.append(min_rtt)
|
|
519
|
+
self.max_stats.append(max_rtt)
|
|
520
|
+
self.avg_stats.append(avg_rtt)
|
|
501
521
|
|
|
502
522
|
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
|
503
523
|
logging.info(color(f'=== {run_counter} Done!', 'magenta'))
|
|
504
524
|
|
|
505
525
|
if self.repeat:
|
|
506
|
-
log_stats('Min
|
|
507
|
-
log_stats('Max
|
|
508
|
-
log_stats('Average
|
|
526
|
+
log_stats('Min RTT', self.min_stats)
|
|
527
|
+
log_stats('Max RTT', self.max_stats)
|
|
528
|
+
log_stats('Average RTT', self.avg_stats)
|
|
509
529
|
|
|
510
530
|
if self.repeat:
|
|
511
531
|
logging.info(color('--- End of runs', 'blue'))
|
|
512
532
|
|
|
513
|
-
async def send_next_ping(self):
|
|
514
|
-
if self.pace:
|
|
515
|
-
await asyncio.sleep(self.pace / 1000)
|
|
516
|
-
|
|
517
|
-
packet = struct.pack(
|
|
518
|
-
'>bbI',
|
|
519
|
-
PacketType.SEQUENCE,
|
|
520
|
-
(
|
|
521
|
-
PACKET_FLAG_LAST
|
|
522
|
-
if self.current_packet_index == self.tx_packet_count - 1
|
|
523
|
-
else 0
|
|
524
|
-
),
|
|
525
|
-
self.current_packet_index,
|
|
526
|
-
) + bytes(self.tx_packet_size - 6)
|
|
527
|
-
logging.info(color(f'Sending packet {self.current_packet_index}', 'yellow'))
|
|
528
|
-
self.ping_sent_time = time.time()
|
|
529
|
-
await self.packet_io.send_packet(packet)
|
|
530
|
-
|
|
531
533
|
def on_packet_received(self, packet):
|
|
532
|
-
elapsed = time.time() - self.ping_sent_time
|
|
533
|
-
|
|
534
534
|
try:
|
|
535
535
|
packet_type, packet_data = parse_packet(packet)
|
|
536
536
|
except ValueError:
|
|
@@ -542,21 +542,23 @@ class Ping:
|
|
|
542
542
|
return
|
|
543
543
|
|
|
544
544
|
if packet_type == PacketType.ACK:
|
|
545
|
-
|
|
546
|
-
|
|
545
|
+
elapsed = time.time() - self.ping_times[packet_index]
|
|
546
|
+
rtt = elapsed * 1000
|
|
547
|
+
self.rtts.append(rtt)
|
|
547
548
|
logging.info(
|
|
548
549
|
color(
|
|
549
|
-
f'<<< Received ACK [{packet_index}],
|
|
550
|
+
f'<<< Received ACK [{packet_index}], RTT={rtt:.2f}ms',
|
|
550
551
|
'green',
|
|
551
552
|
)
|
|
552
553
|
)
|
|
553
554
|
|
|
554
|
-
if packet_index == self.
|
|
555
|
-
self.
|
|
555
|
+
if packet_index == self.next_expected_packet_index:
|
|
556
|
+
self.next_expected_packet_index += 1
|
|
556
557
|
else:
|
|
557
558
|
logging.info(
|
|
558
559
|
color(
|
|
559
|
-
f'!!! Unexpected packet,
|
|
560
|
+
f'!!! Unexpected packet, '
|
|
561
|
+
f'expected {self.next_expected_packet_index} '
|
|
560
562
|
f'but received {packet_index}'
|
|
561
563
|
)
|
|
562
564
|
)
|
|
@@ -565,8 +567,6 @@ class Ping:
|
|
|
565
567
|
self.done.set()
|
|
566
568
|
return
|
|
567
569
|
|
|
568
|
-
AsyncRunner.spawn(self.send_next_ping())
|
|
569
|
-
|
|
570
570
|
|
|
571
571
|
# -----------------------------------------------------------------------------
|
|
572
572
|
# Pong
|
|
@@ -583,8 +583,11 @@ class Pong:
|
|
|
583
583
|
|
|
584
584
|
def reset(self):
|
|
585
585
|
self.expected_packet_index = 0
|
|
586
|
+
self.receive_times = []
|
|
586
587
|
|
|
587
588
|
def on_packet_received(self, packet):
|
|
589
|
+
self.receive_times.append(time.time())
|
|
590
|
+
|
|
588
591
|
try:
|
|
589
592
|
packet_type, packet_data = parse_packet(packet)
|
|
590
593
|
except ValueError:
|
|
@@ -599,10 +602,16 @@ class Pong:
|
|
|
599
602
|
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
|
600
603
|
except ValueError:
|
|
601
604
|
return
|
|
605
|
+
interval = (
|
|
606
|
+
self.receive_times[-1] - self.receive_times[-2]
|
|
607
|
+
if len(self.receive_times) >= 2
|
|
608
|
+
else 0
|
|
609
|
+
)
|
|
602
610
|
logging.info(
|
|
603
611
|
color(
|
|
604
612
|
f'<<< Received packet {packet_index}: '
|
|
605
|
-
f'flags=0x{packet_flags:02X}, {len(packet)} bytes'
|
|
613
|
+
f'flags=0x{packet_flags:02X}, {len(packet)} bytes, '
|
|
614
|
+
f'interval={interval:.4f}',
|
|
606
615
|
'green',
|
|
607
616
|
)
|
|
608
617
|
)
|
|
@@ -623,8 +632,35 @@ class Pong:
|
|
|
623
632
|
)
|
|
624
633
|
)
|
|
625
634
|
|
|
626
|
-
if packet_flags & PACKET_FLAG_LAST
|
|
627
|
-
self.
|
|
635
|
+
if packet_flags & PACKET_FLAG_LAST:
|
|
636
|
+
if len(self.receive_times) >= 3:
|
|
637
|
+
# Show basic stats
|
|
638
|
+
intervals = [
|
|
639
|
+
self.receive_times[i + 1] - self.receive_times[i]
|
|
640
|
+
for i in range(len(self.receive_times) - 1)
|
|
641
|
+
]
|
|
642
|
+
log_stats('Packet intervals', intervals, 3)
|
|
643
|
+
|
|
644
|
+
# Show a histogram
|
|
645
|
+
bin_count = 20
|
|
646
|
+
bins = [0] * bin_count
|
|
647
|
+
interval_min = min(intervals)
|
|
648
|
+
interval_max = max(intervals)
|
|
649
|
+
interval_range = interval_max - interval_min
|
|
650
|
+
bin_thresholds = [
|
|
651
|
+
interval_min + i * (interval_range / bin_count)
|
|
652
|
+
for i in range(bin_count)
|
|
653
|
+
]
|
|
654
|
+
for interval in intervals:
|
|
655
|
+
for i in reversed(range(bin_count)):
|
|
656
|
+
if interval >= bin_thresholds[i]:
|
|
657
|
+
bins[i] += 1
|
|
658
|
+
break
|
|
659
|
+
for i in range(bin_count):
|
|
660
|
+
logging.info(f'@@@ >= {bin_thresholds[i]:.4f}: {bins[i]}')
|
|
661
|
+
|
|
662
|
+
if not self.linger:
|
|
663
|
+
self.done.set()
|
|
628
664
|
|
|
629
665
|
async def run(self):
|
|
630
666
|
await self.done.wait()
|
|
@@ -942,9 +978,12 @@ class RfcommClient(StreamedPacketIO):
|
|
|
942
978
|
channel = await bumble.rfcomm.find_rfcomm_channel_with_uuid(
|
|
943
979
|
connection, self.uuid
|
|
944
980
|
)
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
981
|
+
if channel:
|
|
982
|
+
logging.info(color(f'@@@ Channel number = {channel}', 'cyan'))
|
|
983
|
+
else:
|
|
984
|
+
logging.warning(
|
|
985
|
+
color('!!! No RFComm service with this UUID found', 'red')
|
|
986
|
+
)
|
|
948
987
|
await connection.disconnect()
|
|
949
988
|
return
|
|
950
989
|
|
|
@@ -1054,6 +1093,8 @@ class RfcommServer(StreamedPacketIO):
|
|
|
1054
1093
|
if self.credits_threshold is not None:
|
|
1055
1094
|
dlc.rx_credits_threshold = self.credits_threshold
|
|
1056
1095
|
|
|
1096
|
+
self.ready.set()
|
|
1097
|
+
|
|
1057
1098
|
async def drain(self):
|
|
1058
1099
|
assert self.dlc
|
|
1059
1100
|
await self.dlc.drain()
|
|
@@ -1068,7 +1109,7 @@ class Central(Connection.Listener):
|
|
|
1068
1109
|
transport,
|
|
1069
1110
|
peripheral_address,
|
|
1070
1111
|
classic,
|
|
1071
|
-
|
|
1112
|
+
scenario_factory,
|
|
1072
1113
|
mode_factory,
|
|
1073
1114
|
connection_interval,
|
|
1074
1115
|
phy,
|
|
@@ -1081,7 +1122,7 @@ class Central(Connection.Listener):
|
|
|
1081
1122
|
self.transport = transport
|
|
1082
1123
|
self.peripheral_address = peripheral_address
|
|
1083
1124
|
self.classic = classic
|
|
1084
|
-
self.
|
|
1125
|
+
self.scenario_factory = scenario_factory
|
|
1085
1126
|
self.mode_factory = mode_factory
|
|
1086
1127
|
self.authenticate = authenticate
|
|
1087
1128
|
self.encrypt = encrypt or authenticate
|
|
@@ -1134,7 +1175,7 @@ class Central(Connection.Listener):
|
|
|
1134
1175
|
DEFAULT_CENTRAL_NAME, central_address, hci_source, hci_sink
|
|
1135
1176
|
)
|
|
1136
1177
|
mode = self.mode_factory(self.device)
|
|
1137
|
-
|
|
1178
|
+
scenario = self.scenario_factory(mode)
|
|
1138
1179
|
self.device.classic_enabled = self.classic
|
|
1139
1180
|
|
|
1140
1181
|
# Set up a pairing config factory with minimal requirements.
|
|
@@ -1215,7 +1256,7 @@ class Central(Connection.Listener):
|
|
|
1215
1256
|
|
|
1216
1257
|
await mode.on_connection(self.connection)
|
|
1217
1258
|
|
|
1218
|
-
await
|
|
1259
|
+
await scenario.run()
|
|
1219
1260
|
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
|
1220
1261
|
await self.connection.disconnect()
|
|
1221
1262
|
|
|
@@ -1246,7 +1287,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1246
1287
|
def __init__(
|
|
1247
1288
|
self,
|
|
1248
1289
|
transport,
|
|
1249
|
-
|
|
1290
|
+
scenario_factory,
|
|
1250
1291
|
mode_factory,
|
|
1251
1292
|
classic,
|
|
1252
1293
|
extended_data_length,
|
|
@@ -1254,11 +1295,11 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1254
1295
|
):
|
|
1255
1296
|
self.transport = transport
|
|
1256
1297
|
self.classic = classic
|
|
1257
|
-
self.
|
|
1298
|
+
self.scenario_factory = scenario_factory
|
|
1258
1299
|
self.mode_factory = mode_factory
|
|
1259
1300
|
self.extended_data_length = extended_data_length
|
|
1260
1301
|
self.role_switch = role_switch
|
|
1261
|
-
self.
|
|
1302
|
+
self.scenario = None
|
|
1262
1303
|
self.mode = None
|
|
1263
1304
|
self.device = None
|
|
1264
1305
|
self.connection = None
|
|
@@ -1278,7 +1319,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1278
1319
|
)
|
|
1279
1320
|
self.device.listener = self
|
|
1280
1321
|
self.mode = self.mode_factory(self.device)
|
|
1281
|
-
self.
|
|
1322
|
+
self.scenario = self.scenario_factory(self.mode)
|
|
1282
1323
|
self.device.classic_enabled = self.classic
|
|
1283
1324
|
|
|
1284
1325
|
# Set up a pairing config factory with minimal requirements.
|
|
@@ -1315,7 +1356,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1315
1356
|
print_connection(self.connection)
|
|
1316
1357
|
|
|
1317
1358
|
await self.mode.on_connection(self.connection)
|
|
1318
|
-
await self.
|
|
1359
|
+
await self.scenario.run()
|
|
1319
1360
|
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
|
1320
1361
|
|
|
1321
1362
|
def on_connection(self, connection):
|
|
@@ -1344,7 +1385,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1344
1385
|
def on_disconnection(self, reason):
|
|
1345
1386
|
logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
|
|
1346
1387
|
self.connection = None
|
|
1347
|
-
self.
|
|
1388
|
+
self.scenario.reset()
|
|
1348
1389
|
|
|
1349
1390
|
if self.classic:
|
|
1350
1391
|
AsyncRunner.spawn(self.device.set_discoverable(True))
|
|
@@ -1426,13 +1467,13 @@ def create_mode_factory(ctx, default_mode):
|
|
|
1426
1467
|
|
|
1427
1468
|
|
|
1428
1469
|
# -----------------------------------------------------------------------------
|
|
1429
|
-
def
|
|
1430
|
-
|
|
1431
|
-
if
|
|
1432
|
-
|
|
1470
|
+
def create_scenario_factory(ctx, default_scenario):
|
|
1471
|
+
scenario = ctx.obj['scenario']
|
|
1472
|
+
if scenario is None:
|
|
1473
|
+
scenarion = default_scenario
|
|
1433
1474
|
|
|
1434
|
-
def
|
|
1435
|
-
if
|
|
1475
|
+
def create_scenario(packet_io):
|
|
1476
|
+
if scenario == 'send':
|
|
1436
1477
|
return Sender(
|
|
1437
1478
|
packet_io,
|
|
1438
1479
|
start_delay=ctx.obj['start_delay'],
|
|
@@ -1443,10 +1484,10 @@ def create_role_factory(ctx, default_role):
|
|
|
1443
1484
|
packet_count=ctx.obj['packet_count'],
|
|
1444
1485
|
)
|
|
1445
1486
|
|
|
1446
|
-
if
|
|
1487
|
+
if scenario == 'receive':
|
|
1447
1488
|
return Receiver(packet_io, ctx.obj['linger'])
|
|
1448
1489
|
|
|
1449
|
-
if
|
|
1490
|
+
if scenario == 'ping':
|
|
1450
1491
|
return Ping(
|
|
1451
1492
|
packet_io,
|
|
1452
1493
|
start_delay=ctx.obj['start_delay'],
|
|
@@ -1457,12 +1498,12 @@ def create_role_factory(ctx, default_role):
|
|
|
1457
1498
|
packet_count=ctx.obj['packet_count'],
|
|
1458
1499
|
)
|
|
1459
1500
|
|
|
1460
|
-
if
|
|
1501
|
+
if scenario == 'pong':
|
|
1461
1502
|
return Pong(packet_io, ctx.obj['linger'])
|
|
1462
1503
|
|
|
1463
|
-
raise ValueError('invalid
|
|
1504
|
+
raise ValueError('invalid scenario')
|
|
1464
1505
|
|
|
1465
|
-
return
|
|
1506
|
+
return create_scenario
|
|
1466
1507
|
|
|
1467
1508
|
|
|
1468
1509
|
# -----------------------------------------------------------------------------
|
|
@@ -1470,7 +1511,7 @@ def create_role_factory(ctx, default_role):
|
|
|
1470
1511
|
# -----------------------------------------------------------------------------
|
|
1471
1512
|
@click.group()
|
|
1472
1513
|
@click.option('--device-config', metavar='FILENAME', help='Device configuration file')
|
|
1473
|
-
@click.option('--
|
|
1514
|
+
@click.option('--scenario', type=click.Choice(['send', 'receive', 'ping', 'pong']))
|
|
1474
1515
|
@click.option(
|
|
1475
1516
|
'--mode',
|
|
1476
1517
|
type=click.Choice(
|
|
@@ -1503,7 +1544,7 @@ def create_role_factory(ctx, default_role):
|
|
|
1503
1544
|
'--rfcomm-channel',
|
|
1504
1545
|
type=int,
|
|
1505
1546
|
default=DEFAULT_RFCOMM_CHANNEL,
|
|
1506
|
-
help='RFComm channel to use',
|
|
1547
|
+
help='RFComm channel to use (specify 0 for channel discovery via SDP)',
|
|
1507
1548
|
)
|
|
1508
1549
|
@click.option(
|
|
1509
1550
|
'--rfcomm-uuid',
|
|
@@ -1565,7 +1606,7 @@ def create_role_factory(ctx, default_role):
|
|
|
1565
1606
|
metavar='SIZE',
|
|
1566
1607
|
type=click.IntRange(8, 8192),
|
|
1567
1608
|
default=500,
|
|
1568
|
-
help='Packet size (
|
|
1609
|
+
help='Packet size (send or ping scenario)',
|
|
1569
1610
|
)
|
|
1570
1611
|
@click.option(
|
|
1571
1612
|
'--packet-count',
|
|
@@ -1573,7 +1614,7 @@ def create_role_factory(ctx, default_role):
|
|
|
1573
1614
|
metavar='COUNT',
|
|
1574
1615
|
type=int,
|
|
1575
1616
|
default=10,
|
|
1576
|
-
help='Packet count (
|
|
1617
|
+
help='Packet count (send or ping scenario)',
|
|
1577
1618
|
)
|
|
1578
1619
|
@click.option(
|
|
1579
1620
|
'--start-delay',
|
|
@@ -1581,7 +1622,7 @@ def create_role_factory(ctx, default_role):
|
|
|
1581
1622
|
metavar='SECONDS',
|
|
1582
1623
|
type=int,
|
|
1583
1624
|
default=1,
|
|
1584
|
-
help='Start delay (
|
|
1625
|
+
help='Start delay (send or ping scenario)',
|
|
1585
1626
|
)
|
|
1586
1627
|
@click.option(
|
|
1587
1628
|
'--repeat',
|
|
@@ -1589,7 +1630,7 @@ def create_role_factory(ctx, default_role):
|
|
|
1589
1630
|
type=int,
|
|
1590
1631
|
default=0,
|
|
1591
1632
|
help=(
|
|
1592
|
-
'Repeat the run N times (
|
|
1633
|
+
'Repeat the run N times (send and ping scenario)'
|
|
1593
1634
|
'(0, which is the fault, to run just once) '
|
|
1594
1635
|
),
|
|
1595
1636
|
)
|
|
@@ -1613,13 +1654,13 @@ def create_role_factory(ctx, default_role):
|
|
|
1613
1654
|
@click.option(
|
|
1614
1655
|
'--linger',
|
|
1615
1656
|
is_flag=True,
|
|
1616
|
-
help="Don't exit at the end of a run (
|
|
1657
|
+
help="Don't exit at the end of a run (receive and pong scenarios)",
|
|
1617
1658
|
)
|
|
1618
1659
|
@click.pass_context
|
|
1619
1660
|
def bench(
|
|
1620
1661
|
ctx,
|
|
1621
1662
|
device_config,
|
|
1622
|
-
|
|
1663
|
+
scenario,
|
|
1623
1664
|
mode,
|
|
1624
1665
|
att_mtu,
|
|
1625
1666
|
extended_data_length,
|
|
@@ -1645,7 +1686,7 @@ def bench(
|
|
|
1645
1686
|
):
|
|
1646
1687
|
ctx.ensure_object(dict)
|
|
1647
1688
|
ctx.obj['device_config'] = device_config
|
|
1648
|
-
ctx.obj['
|
|
1689
|
+
ctx.obj['scenario'] = scenario
|
|
1649
1690
|
ctx.obj['mode'] = mode
|
|
1650
1691
|
ctx.obj['att_mtu'] = att_mtu
|
|
1651
1692
|
ctx.obj['rfcomm_channel'] = rfcomm_channel
|
|
@@ -1699,7 +1740,7 @@ def central(
|
|
|
1699
1740
|
ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
|
|
1700
1741
|
):
|
|
1701
1742
|
"""Run as a central (initiates the connection)"""
|
|
1702
|
-
|
|
1743
|
+
scenario_factory = create_scenario_factory(ctx, 'send')
|
|
1703
1744
|
mode_factory = create_mode_factory(ctx, 'gatt-client')
|
|
1704
1745
|
classic = ctx.obj['classic']
|
|
1705
1746
|
|
|
@@ -1708,7 +1749,7 @@ def central(
|
|
|
1708
1749
|
transport,
|
|
1709
1750
|
peripheral_address,
|
|
1710
1751
|
classic,
|
|
1711
|
-
|
|
1752
|
+
scenario_factory,
|
|
1712
1753
|
mode_factory,
|
|
1713
1754
|
connection_interval,
|
|
1714
1755
|
phy,
|
|
@@ -1726,13 +1767,13 @@ def central(
|
|
|
1726
1767
|
@click.pass_context
|
|
1727
1768
|
def peripheral(ctx, transport):
|
|
1728
1769
|
"""Run as a peripheral (waits for a connection)"""
|
|
1729
|
-
|
|
1770
|
+
scenario_factory = create_scenario_factory(ctx, 'receive')
|
|
1730
1771
|
mode_factory = create_mode_factory(ctx, 'gatt-server')
|
|
1731
1772
|
|
|
1732
1773
|
async def run_peripheral():
|
|
1733
1774
|
await Peripheral(
|
|
1734
1775
|
transport,
|
|
1735
|
-
|
|
1776
|
+
scenario_factory,
|
|
1736
1777
|
mode_factory,
|
|
1737
1778
|
ctx.obj['classic'],
|
|
1738
1779
|
ctx.obj['extended_data_length'],
|
|
@@ -1743,7 +1784,11 @@ def peripheral(ctx, transport):
|
|
|
1743
1784
|
|
|
1744
1785
|
|
|
1745
1786
|
def main():
|
|
1746
|
-
logging.basicConfig(
|
|
1787
|
+
logging.basicConfig(
|
|
1788
|
+
level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
|
|
1789
|
+
format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
|
|
1790
|
+
datefmt="%H:%M:%S",
|
|
1791
|
+
)
|
|
1747
1792
|
bench()
|
|
1748
1793
|
|
|
1749
1794
|
|
bumble/apps/hci_bridge.py
CHANGED
bumble/apps/lea_unicast/app.py
CHANGED
|
@@ -486,7 +486,12 @@ class Speaker:
|
|
|
486
486
|
|
|
487
487
|
def on_pdu(pdu: HCI_IsoDataPacket, ase: ascs.AseStateMachine):
|
|
488
488
|
codec_config = ase.codec_specific_configuration
|
|
489
|
-
|
|
489
|
+
if (
|
|
490
|
+
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
491
|
+
or codec_config.frame_duration is None
|
|
492
|
+
or codec_config.audio_channel_allocation is None
|
|
493
|
+
):
|
|
494
|
+
return
|
|
490
495
|
pcm = decode(
|
|
491
496
|
codec_config.frame_duration.us,
|
|
492
497
|
codec_config.audio_channel_allocation.channel_count,
|
|
@@ -495,11 +500,17 @@ class Speaker:
|
|
|
495
500
|
self.device.abort_on('disconnection', self.ui_server.send_audio(pcm))
|
|
496
501
|
|
|
497
502
|
def on_ase_state_change(ase: ascs.AseStateMachine) -> None:
|
|
503
|
+
codec_config = ase.codec_specific_configuration
|
|
498
504
|
if ase.state == ascs.AseStateMachine.State.STREAMING:
|
|
499
|
-
codec_config = ase.codec_specific_configuration
|
|
500
|
-
assert isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
501
|
-
assert ase.cis_link
|
|
502
505
|
if ase.role == ascs.AudioRole.SOURCE:
|
|
506
|
+
if (
|
|
507
|
+
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
508
|
+
or ase.cis_link is None
|
|
509
|
+
or codec_config.octets_per_codec_frame is None
|
|
510
|
+
or codec_config.frame_duration is None
|
|
511
|
+
or codec_config.codec_frames_per_sdu is None
|
|
512
|
+
):
|
|
513
|
+
return
|
|
503
514
|
ase.cis_link.abort_on(
|
|
504
515
|
'disconnection',
|
|
505
516
|
lc3_source_task(
|
|
@@ -514,10 +525,17 @@ class Speaker:
|
|
|
514
525
|
),
|
|
515
526
|
)
|
|
516
527
|
else:
|
|
528
|
+
if not ase.cis_link:
|
|
529
|
+
return
|
|
517
530
|
ase.cis_link.sink = functools.partial(on_pdu, ase=ase)
|
|
518
531
|
elif ase.state == ascs.AseStateMachine.State.CODEC_CONFIGURED:
|
|
519
|
-
|
|
520
|
-
|
|
532
|
+
if (
|
|
533
|
+
not isinstance(codec_config, bap.CodecSpecificConfiguration)
|
|
534
|
+
or codec_config.sampling_frequency is None
|
|
535
|
+
or codec_config.frame_duration is None
|
|
536
|
+
or codec_config.audio_channel_allocation is None
|
|
537
|
+
):
|
|
538
|
+
return
|
|
521
539
|
if ase.role == ascs.AudioRole.SOURCE:
|
|
522
540
|
setup_encoders(
|
|
523
541
|
codec_config.sampling_frequency.hz,
|