bumble 0.0.204__py3-none-any.whl → 0.0.207__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 +626 -87
- bumble/apps/bench.py +225 -147
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +4 -1
- bumble/device.py +993 -48
- bumble/gatt.py +35 -6
- bumble/gatt_client.py +14 -2
- bumble/hci.py +812 -14
- bumble/host.py +359 -63
- bumble/l2cap.py +3 -16
- bumble/profiles/aics.py +19 -38
- bumble/profiles/ascs.py +6 -18
- bumble/profiles/asha.py +5 -5
- bumble/profiles/bass.py +10 -19
- bumble/profiles/gatt_service.py +166 -0
- bumble/profiles/gmap.py +193 -0
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/pacs.py +48 -16
- bumble/profiles/tmap.py +3 -9
- bumble/profiles/{vcp.py → vcs.py} +33 -28
- bumble/profiles/vocs.py +54 -85
- bumble/sdp.py +223 -93
- bumble/smp.py +1 -1
- bumble/utils.py +2 -2
- bumble/vendor/android/hci.py +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/METADATA +12 -10
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/RECORD +37 -34
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/WHEEL +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/entry_points.txt +1 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/LICENSE +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/top_level.txt +0 -0
bumble/apps/bench.py
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
import asyncio
|
|
19
|
+
import dataclasses
|
|
19
20
|
import enum
|
|
20
21
|
import logging
|
|
21
22
|
import os
|
|
@@ -97,34 +98,6 @@ DEFAULT_RFCOMM_MTU = 2048
|
|
|
97
98
|
# -----------------------------------------------------------------------------
|
|
98
99
|
# Utils
|
|
99
100
|
# -----------------------------------------------------------------------------
|
|
100
|
-
def parse_packet(packet):
|
|
101
|
-
if len(packet) < 1:
|
|
102
|
-
logging.info(
|
|
103
|
-
color(f'!!! Packet too short (got {len(packet)} bytes, need >= 1)', 'red')
|
|
104
|
-
)
|
|
105
|
-
raise ValueError('packet too short')
|
|
106
|
-
|
|
107
|
-
try:
|
|
108
|
-
packet_type = PacketType(packet[0])
|
|
109
|
-
except ValueError:
|
|
110
|
-
logging.info(color(f'!!! Invalid packet type 0x{packet[0]:02X}', 'red'))
|
|
111
|
-
raise
|
|
112
|
-
|
|
113
|
-
return (packet_type, packet[1:])
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def parse_packet_sequence(packet_data):
|
|
117
|
-
if len(packet_data) < 5:
|
|
118
|
-
logging.info(
|
|
119
|
-
color(
|
|
120
|
-
f'!!!Packet too short (got {len(packet_data)} bytes, need >= 5)',
|
|
121
|
-
'red',
|
|
122
|
-
)
|
|
123
|
-
)
|
|
124
|
-
raise ValueError('packet too short')
|
|
125
|
-
return struct.unpack_from('>bI', packet_data, 0)
|
|
126
|
-
|
|
127
|
-
|
|
128
101
|
def le_phy_name(phy_id):
|
|
129
102
|
return {HCI_LE_1M_PHY: '1M', HCI_LE_2M_PHY: '2M', HCI_LE_CODED_PHY: 'CODED'}.get(
|
|
130
103
|
phy_id, HCI_Constant.le_phy_name(phy_id)
|
|
@@ -225,13 +198,135 @@ async def switch_roles(connection, role):
|
|
|
225
198
|
logging.info(f'{color("### Role switch failed:", "red")} {error}')
|
|
226
199
|
|
|
227
200
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
201
|
+
# -----------------------------------------------------------------------------
|
|
202
|
+
# Packet
|
|
203
|
+
# -----------------------------------------------------------------------------
|
|
204
|
+
@dataclasses.dataclass
|
|
205
|
+
class Packet:
|
|
206
|
+
class PacketType(enum.IntEnum):
|
|
207
|
+
RESET = 0
|
|
208
|
+
SEQUENCE = 1
|
|
209
|
+
ACK = 2
|
|
210
|
+
|
|
211
|
+
class PacketFlags(enum.IntFlag):
|
|
212
|
+
LAST = 1
|
|
213
|
+
|
|
214
|
+
packet_type: PacketType
|
|
215
|
+
flags: PacketFlags = PacketFlags(0)
|
|
216
|
+
sequence: int = 0
|
|
217
|
+
timestamp: int = 0
|
|
218
|
+
payload: bytes = b""
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def from_bytes(cls, data: bytes):
|
|
222
|
+
if len(data) < 1:
|
|
223
|
+
logging.warning(
|
|
224
|
+
color(f'!!! Packet too short (got {len(data)} bytes, need >= 1)', 'red')
|
|
225
|
+
)
|
|
226
|
+
raise ValueError('packet too short')
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
packet_type = cls.PacketType(data[0])
|
|
230
|
+
except ValueError:
|
|
231
|
+
logging.warning(color(f'!!! Invalid packet type 0x{data[0]:02X}', 'red'))
|
|
232
|
+
raise
|
|
233
|
+
|
|
234
|
+
if packet_type == cls.PacketType.RESET:
|
|
235
|
+
return cls(packet_type)
|
|
236
|
+
|
|
237
|
+
flags = cls.PacketFlags(data[1])
|
|
238
|
+
(sequence,) = struct.unpack_from("<I", data, 2)
|
|
239
|
+
|
|
240
|
+
if packet_type == cls.PacketType.ACK:
|
|
241
|
+
if len(data) < 6:
|
|
242
|
+
logging.warning(
|
|
243
|
+
color(
|
|
244
|
+
f'!!! Packet too short (got {len(data)} bytes, need >= 6)',
|
|
245
|
+
'red',
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
return cls(packet_type, flags, sequence)
|
|
249
|
+
|
|
250
|
+
if len(data) < 10:
|
|
251
|
+
logging.warning(
|
|
252
|
+
color(
|
|
253
|
+
f'!!! Packet too short (got {len(data)} bytes, need >= 10)', 'red'
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
raise ValueError('packet too short')
|
|
257
|
+
|
|
258
|
+
(timestamp,) = struct.unpack_from("<I", data, 6)
|
|
259
|
+
return cls(packet_type, flags, sequence, timestamp, data[10:])
|
|
260
|
+
|
|
261
|
+
def __bytes__(self):
|
|
262
|
+
if self.packet_type == self.PacketType.RESET:
|
|
263
|
+
return bytes([self.packet_type])
|
|
264
|
+
|
|
265
|
+
if self.packet_type == self.PacketType.ACK:
|
|
266
|
+
return struct.pack("<BBI", self.packet_type, self.flags, self.sequence)
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
struct.pack(
|
|
270
|
+
"<BBII", self.packet_type, self.flags, self.sequence, self.timestamp
|
|
271
|
+
)
|
|
272
|
+
+ self.payload
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# -----------------------------------------------------------------------------
|
|
277
|
+
# Jitter Stats
|
|
278
|
+
# -----------------------------------------------------------------------------
|
|
279
|
+
class JitterStats:
|
|
280
|
+
def __init__(self):
|
|
281
|
+
self.reset()
|
|
282
|
+
|
|
283
|
+
def reset(self):
|
|
284
|
+
self.packets = []
|
|
285
|
+
self.receive_times = []
|
|
286
|
+
self.jitter = []
|
|
287
|
+
|
|
288
|
+
def on_packet_received(self, packet):
|
|
289
|
+
now = time.time()
|
|
290
|
+
self.packets.append(packet)
|
|
291
|
+
self.receive_times.append(now)
|
|
292
|
+
|
|
293
|
+
if packet.timestamp and len(self.packets) > 1:
|
|
294
|
+
expected_time = (
|
|
295
|
+
self.receive_times[0]
|
|
296
|
+
+ (packet.timestamp - self.packets[0].timestamp) / 1000000
|
|
297
|
+
)
|
|
298
|
+
jitter = now - expected_time
|
|
299
|
+
else:
|
|
300
|
+
jitter = 0.0
|
|
232
301
|
|
|
302
|
+
self.jitter.append(jitter)
|
|
303
|
+
return jitter
|
|
233
304
|
|
|
234
|
-
|
|
305
|
+
def show_stats(self):
|
|
306
|
+
if len(self.jitter) < 3:
|
|
307
|
+
return
|
|
308
|
+
average = sum(self.jitter) / len(self.jitter)
|
|
309
|
+
adjusted = [jitter - average for jitter in self.jitter]
|
|
310
|
+
|
|
311
|
+
log_stats('Jitter (signed)', adjusted, 3)
|
|
312
|
+
log_stats('Jitter (absolute)', [abs(jitter) for jitter in adjusted], 3)
|
|
313
|
+
|
|
314
|
+
# Show a histogram
|
|
315
|
+
bin_count = 20
|
|
316
|
+
bins = [0] * bin_count
|
|
317
|
+
interval_min = min(adjusted)
|
|
318
|
+
interval_max = max(adjusted)
|
|
319
|
+
interval_range = interval_max - interval_min
|
|
320
|
+
bin_thresholds = [
|
|
321
|
+
interval_min + i * (interval_range / bin_count) for i in range(bin_count)
|
|
322
|
+
]
|
|
323
|
+
for jitter in adjusted:
|
|
324
|
+
for i in reversed(range(bin_count)):
|
|
325
|
+
if jitter >= bin_thresholds[i]:
|
|
326
|
+
bins[i] += 1
|
|
327
|
+
break
|
|
328
|
+
for i in range(bin_count):
|
|
329
|
+
logging.info(f'@@@ >= {bin_thresholds[i]:.4f}: {bins[i]}')
|
|
235
330
|
|
|
236
331
|
|
|
237
332
|
# -----------------------------------------------------------------------------
|
|
@@ -281,19 +376,37 @@ class Sender:
|
|
|
281
376
|
await asyncio.sleep(self.tx_start_delay)
|
|
282
377
|
|
|
283
378
|
logging.info(color('=== Sending RESET', 'magenta'))
|
|
284
|
-
await self.packet_io.send_packet(
|
|
379
|
+
await self.packet_io.send_packet(
|
|
380
|
+
bytes(Packet(packet_type=Packet.PacketType.RESET))
|
|
381
|
+
)
|
|
382
|
+
|
|
285
383
|
self.start_time = time.time()
|
|
286
384
|
self.bytes_sent = 0
|
|
287
385
|
for tx_i in range(self.tx_packet_count):
|
|
288
|
-
|
|
289
|
-
|
|
386
|
+
if self.pace > 0:
|
|
387
|
+
# Wait until it is time to send the next packet
|
|
388
|
+
target_time = self.start_time + (tx_i * self.pace / 1000)
|
|
389
|
+
now = time.time()
|
|
390
|
+
if now < target_time:
|
|
391
|
+
await asyncio.sleep(target_time - now)
|
|
392
|
+
else:
|
|
393
|
+
await self.packet_io.drain()
|
|
394
|
+
|
|
395
|
+
packet = bytes(
|
|
396
|
+
Packet(
|
|
397
|
+
packet_type=Packet.PacketType.SEQUENCE,
|
|
398
|
+
flags=(
|
|
399
|
+
Packet.PacketFlags.LAST
|
|
400
|
+
if tx_i == self.tx_packet_count - 1
|
|
401
|
+
else 0
|
|
402
|
+
),
|
|
403
|
+
sequence=tx_i,
|
|
404
|
+
timestamp=int((time.time() - self.start_time) * 1000000),
|
|
405
|
+
payload=bytes(
|
|
406
|
+
self.tx_packet_size - 10 - self.packet_io.overhead_size
|
|
407
|
+
),
|
|
408
|
+
)
|
|
290
409
|
)
|
|
291
|
-
packet = struct.pack(
|
|
292
|
-
'>bbI',
|
|
293
|
-
PacketType.SEQUENCE,
|
|
294
|
-
packet_flags,
|
|
295
|
-
tx_i,
|
|
296
|
-
) + bytes(self.tx_packet_size - 6 - self.packet_io.overhead_size)
|
|
297
410
|
logging.info(
|
|
298
411
|
color(
|
|
299
412
|
f'Sending packet {tx_i}: {self.tx_packet_size} bytes', 'yellow'
|
|
@@ -302,14 +415,6 @@ class Sender:
|
|
|
302
415
|
self.bytes_sent += len(packet)
|
|
303
416
|
await self.packet_io.send_packet(packet)
|
|
304
417
|
|
|
305
|
-
if self.pace is None:
|
|
306
|
-
continue
|
|
307
|
-
|
|
308
|
-
if self.pace > 0:
|
|
309
|
-
await asyncio.sleep(self.pace / 1000)
|
|
310
|
-
else:
|
|
311
|
-
await self.packet_io.drain()
|
|
312
|
-
|
|
313
418
|
await self.done.wait()
|
|
314
419
|
|
|
315
420
|
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
|
@@ -321,13 +426,13 @@ class Sender:
|
|
|
321
426
|
if self.repeat:
|
|
322
427
|
logging.info(color('--- End of runs', 'blue'))
|
|
323
428
|
|
|
324
|
-
def on_packet_received(self,
|
|
429
|
+
def on_packet_received(self, data):
|
|
325
430
|
try:
|
|
326
|
-
|
|
431
|
+
packet = Packet.from_bytes(data)
|
|
327
432
|
except ValueError:
|
|
328
433
|
return
|
|
329
434
|
|
|
330
|
-
if packet_type == PacketType.ACK:
|
|
435
|
+
if packet.packet_type == Packet.PacketType.ACK:
|
|
331
436
|
elapsed = time.time() - self.start_time
|
|
332
437
|
average_tx_speed = self.bytes_sent / elapsed
|
|
333
438
|
self.stats.append(average_tx_speed)
|
|
@@ -350,52 +455,53 @@ class Receiver:
|
|
|
350
455
|
last_timestamp: float
|
|
351
456
|
|
|
352
457
|
def __init__(self, packet_io, linger):
|
|
353
|
-
self.
|
|
458
|
+
self.jitter_stats = JitterStats()
|
|
354
459
|
self.packet_io = packet_io
|
|
355
460
|
self.packet_io.packet_listener = self
|
|
356
461
|
self.linger = linger
|
|
357
462
|
self.done = asyncio.Event()
|
|
463
|
+
self.reset()
|
|
358
464
|
|
|
359
465
|
def reset(self):
|
|
360
466
|
self.expected_packet_index = 0
|
|
361
467
|
self.measurements = [(time.time(), 0)]
|
|
362
468
|
self.total_bytes_received = 0
|
|
469
|
+
self.jitter_stats.reset()
|
|
363
470
|
|
|
364
|
-
def on_packet_received(self,
|
|
471
|
+
def on_packet_received(self, data):
|
|
365
472
|
try:
|
|
366
|
-
|
|
473
|
+
packet = Packet.from_bytes(data)
|
|
367
474
|
except ValueError:
|
|
475
|
+
logging.exception("invalid packet")
|
|
368
476
|
return
|
|
369
477
|
|
|
370
|
-
if packet_type == PacketType.RESET:
|
|
478
|
+
if packet.packet_type == Packet.PacketType.RESET:
|
|
371
479
|
logging.info(color('=== Received RESET', 'magenta'))
|
|
372
480
|
self.reset()
|
|
373
481
|
return
|
|
374
482
|
|
|
375
|
-
|
|
376
|
-
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
|
377
|
-
except ValueError:
|
|
378
|
-
return
|
|
483
|
+
jitter = self.jitter_stats.on_packet_received(packet)
|
|
379
484
|
logging.info(
|
|
380
|
-
f'<<< Received packet {
|
|
381
|
-
f'flags=
|
|
382
|
-
f'{
|
|
485
|
+
f'<<< Received packet {packet.sequence}: '
|
|
486
|
+
f'flags={packet.flags}, '
|
|
487
|
+
f'jitter={jitter:.4f}, '
|
|
488
|
+
f'{len(data) + self.packet_io.overhead_size} bytes',
|
|
383
489
|
)
|
|
384
490
|
|
|
385
|
-
if
|
|
491
|
+
if packet.sequence != self.expected_packet_index:
|
|
386
492
|
logging.info(
|
|
387
493
|
color(
|
|
388
494
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
|
389
|
-
f'but received {
|
|
495
|
+
f'but received {packet.sequence}'
|
|
390
496
|
)
|
|
391
497
|
)
|
|
392
498
|
|
|
393
499
|
now = time.time()
|
|
394
500
|
elapsed_since_start = now - self.measurements[0][0]
|
|
395
501
|
elapsed_since_last = now - self.measurements[-1][0]
|
|
396
|
-
self.measurements.append((now, len(
|
|
397
|
-
self.total_bytes_received += len(
|
|
398
|
-
instant_rx_speed = len(
|
|
502
|
+
self.measurements.append((now, len(data)))
|
|
503
|
+
self.total_bytes_received += len(data)
|
|
504
|
+
instant_rx_speed = len(data) / elapsed_since_last
|
|
399
505
|
average_rx_speed = self.total_bytes_received / elapsed_since_start
|
|
400
506
|
window = self.measurements[-64:]
|
|
401
507
|
windowed_rx_speed = sum(measurement[1] for measurement in window[1:]) / (
|
|
@@ -411,15 +517,17 @@ class Receiver:
|
|
|
411
517
|
)
|
|
412
518
|
)
|
|
413
519
|
|
|
414
|
-
self.expected_packet_index =
|
|
520
|
+
self.expected_packet_index = packet.sequence + 1
|
|
415
521
|
|
|
416
|
-
if
|
|
522
|
+
if packet.flags & Packet.PacketFlags.LAST:
|
|
417
523
|
AsyncRunner.spawn(
|
|
418
524
|
self.packet_io.send_packet(
|
|
419
|
-
|
|
525
|
+
bytes(Packet(Packet.PacketType.ACK, packet.flags, packet.sequence))
|
|
420
526
|
)
|
|
421
527
|
)
|
|
422
528
|
logging.info(color('@@@ Received last packet', 'green'))
|
|
529
|
+
self.jitter_stats.show_stats()
|
|
530
|
+
|
|
423
531
|
if not self.linger:
|
|
424
532
|
self.done.set()
|
|
425
533
|
|
|
@@ -479,25 +587,32 @@ class Ping:
|
|
|
479
587
|
await asyncio.sleep(self.tx_start_delay)
|
|
480
588
|
|
|
481
589
|
logging.info(color('=== Sending RESET', 'magenta'))
|
|
482
|
-
await self.packet_io.send_packet(bytes(
|
|
590
|
+
await self.packet_io.send_packet(bytes(Packet(Packet.PacketType.RESET)))
|
|
483
591
|
|
|
484
|
-
packet_interval = self.pace / 1000
|
|
485
592
|
start_time = time.time()
|
|
486
593
|
self.next_expected_packet_index = 0
|
|
487
594
|
for i in range(self.tx_packet_count):
|
|
488
|
-
target_time = start_time + (i *
|
|
595
|
+
target_time = start_time + (i * self.pace / 1000)
|
|
489
596
|
now = time.time()
|
|
490
597
|
if now < target_time:
|
|
491
598
|
await asyncio.sleep(target_time - now)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
599
|
+
now = time.time()
|
|
600
|
+
|
|
601
|
+
packet = bytes(
|
|
602
|
+
Packet(
|
|
603
|
+
packet_type=Packet.PacketType.SEQUENCE,
|
|
604
|
+
flags=(
|
|
605
|
+
Packet.PacketFlags.LAST
|
|
606
|
+
if i == self.tx_packet_count - 1
|
|
607
|
+
else 0
|
|
608
|
+
),
|
|
609
|
+
sequence=i,
|
|
610
|
+
timestamp=int((now - start_time) * 1000000),
|
|
611
|
+
payload=bytes(self.tx_packet_size - 10),
|
|
612
|
+
)
|
|
613
|
+
)
|
|
499
614
|
logging.info(color(f'Sending packet {i}', 'yellow'))
|
|
500
|
-
self.ping_times.append(
|
|
615
|
+
self.ping_times.append(now)
|
|
501
616
|
await self.packet_io.send_packet(packet)
|
|
502
617
|
|
|
503
618
|
await self.done.wait()
|
|
@@ -531,40 +646,35 @@ class Ping:
|
|
|
531
646
|
if self.repeat:
|
|
532
647
|
logging.info(color('--- End of runs', 'blue'))
|
|
533
648
|
|
|
534
|
-
def on_packet_received(self,
|
|
649
|
+
def on_packet_received(self, data):
|
|
535
650
|
try:
|
|
536
|
-
|
|
651
|
+
packet = Packet.from_bytes(data)
|
|
537
652
|
except ValueError:
|
|
538
653
|
return
|
|
539
654
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
except ValueError:
|
|
543
|
-
return
|
|
544
|
-
|
|
545
|
-
if packet_type == PacketType.ACK:
|
|
546
|
-
elapsed = time.time() - self.ping_times[packet_index]
|
|
655
|
+
if packet.packet_type == Packet.PacketType.ACK:
|
|
656
|
+
elapsed = time.time() - self.ping_times[packet.sequence]
|
|
547
657
|
rtt = elapsed * 1000
|
|
548
658
|
self.rtts.append(rtt)
|
|
549
659
|
logging.info(
|
|
550
660
|
color(
|
|
551
|
-
f'<<< Received ACK [{
|
|
661
|
+
f'<<< Received ACK [{packet.sequence}], RTT={rtt:.2f}ms',
|
|
552
662
|
'green',
|
|
553
663
|
)
|
|
554
664
|
)
|
|
555
665
|
|
|
556
|
-
if
|
|
666
|
+
if packet.sequence == self.next_expected_packet_index:
|
|
557
667
|
self.next_expected_packet_index += 1
|
|
558
668
|
else:
|
|
559
669
|
logging.info(
|
|
560
670
|
color(
|
|
561
671
|
f'!!! Unexpected packet, '
|
|
562
672
|
f'expected {self.next_expected_packet_index} '
|
|
563
|
-
f'but received {
|
|
673
|
+
f'but received {packet.sequence}'
|
|
564
674
|
)
|
|
565
675
|
)
|
|
566
676
|
|
|
567
|
-
if
|
|
677
|
+
if packet.flags & Packet.PacketFlags.LAST:
|
|
568
678
|
self.done.set()
|
|
569
679
|
return
|
|
570
680
|
|
|
@@ -576,89 +686,56 @@ class Pong:
|
|
|
576
686
|
expected_packet_index: int
|
|
577
687
|
|
|
578
688
|
def __init__(self, packet_io, linger):
|
|
579
|
-
self.
|
|
689
|
+
self.jitter_stats = JitterStats()
|
|
580
690
|
self.packet_io = packet_io
|
|
581
691
|
self.packet_io.packet_listener = self
|
|
582
692
|
self.linger = linger
|
|
583
693
|
self.done = asyncio.Event()
|
|
694
|
+
self.reset()
|
|
584
695
|
|
|
585
696
|
def reset(self):
|
|
586
697
|
self.expected_packet_index = 0
|
|
587
|
-
self.
|
|
588
|
-
|
|
589
|
-
def on_packet_received(self, packet):
|
|
590
|
-
self.receive_times.append(time.time())
|
|
698
|
+
self.jitter_stats.reset()
|
|
591
699
|
|
|
700
|
+
def on_packet_received(self, data):
|
|
592
701
|
try:
|
|
593
|
-
|
|
702
|
+
packet = Packet.from_bytes(data)
|
|
594
703
|
except ValueError:
|
|
595
704
|
return
|
|
596
705
|
|
|
597
|
-
if packet_type == PacketType.RESET:
|
|
706
|
+
if packet.packet_type == Packet.PacketType.RESET:
|
|
598
707
|
logging.info(color('=== Received RESET', 'magenta'))
|
|
599
708
|
self.reset()
|
|
600
709
|
return
|
|
601
710
|
|
|
602
|
-
|
|
603
|
-
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
|
604
|
-
except ValueError:
|
|
605
|
-
return
|
|
606
|
-
interval = (
|
|
607
|
-
self.receive_times[-1] - self.receive_times[-2]
|
|
608
|
-
if len(self.receive_times) >= 2
|
|
609
|
-
else 0
|
|
610
|
-
)
|
|
711
|
+
jitter = self.jitter_stats.on_packet_received(packet)
|
|
611
712
|
logging.info(
|
|
612
713
|
color(
|
|
613
|
-
f'<<< Received packet {
|
|
614
|
-
f'flags=
|
|
615
|
-
f'
|
|
714
|
+
f'<<< Received packet {packet.sequence}: '
|
|
715
|
+
f'flags={packet.flags}, {len(data)} bytes, '
|
|
716
|
+
f'jitter={jitter:.4f}',
|
|
616
717
|
'green',
|
|
617
718
|
)
|
|
618
719
|
)
|
|
619
720
|
|
|
620
|
-
if
|
|
721
|
+
if packet.sequence != self.expected_packet_index:
|
|
621
722
|
logging.info(
|
|
622
723
|
color(
|
|
623
724
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
|
624
|
-
f'but received {
|
|
725
|
+
f'but received {packet.sequence}'
|
|
625
726
|
)
|
|
626
727
|
)
|
|
627
728
|
|
|
628
|
-
self.expected_packet_index =
|
|
729
|
+
self.expected_packet_index = packet.sequence + 1
|
|
629
730
|
|
|
630
731
|
AsyncRunner.spawn(
|
|
631
732
|
self.packet_io.send_packet(
|
|
632
|
-
|
|
733
|
+
bytes(Packet(Packet.PacketType.ACK, packet.flags, packet.sequence))
|
|
633
734
|
)
|
|
634
735
|
)
|
|
635
736
|
|
|
636
|
-
if
|
|
637
|
-
|
|
638
|
-
# Show basic stats
|
|
639
|
-
intervals = [
|
|
640
|
-
self.receive_times[i + 1] - self.receive_times[i]
|
|
641
|
-
for i in range(len(self.receive_times) - 1)
|
|
642
|
-
]
|
|
643
|
-
log_stats('Packet intervals', intervals, 3)
|
|
644
|
-
|
|
645
|
-
# Show a histogram
|
|
646
|
-
bin_count = 20
|
|
647
|
-
bins = [0] * bin_count
|
|
648
|
-
interval_min = min(intervals)
|
|
649
|
-
interval_max = max(intervals)
|
|
650
|
-
interval_range = interval_max - interval_min
|
|
651
|
-
bin_thresholds = [
|
|
652
|
-
interval_min + i * (interval_range / bin_count)
|
|
653
|
-
for i in range(bin_count)
|
|
654
|
-
]
|
|
655
|
-
for interval in intervals:
|
|
656
|
-
for i in reversed(range(bin_count)):
|
|
657
|
-
if interval >= bin_thresholds[i]:
|
|
658
|
-
bins[i] += 1
|
|
659
|
-
break
|
|
660
|
-
for i in range(bin_count):
|
|
661
|
-
logging.info(f'@@@ >= {bin_thresholds[i]:.4f}: {bins[i]}')
|
|
737
|
+
if packet.flags & Packet.PacketFlags.LAST:
|
|
738
|
+
self.jitter_stats.show_stats()
|
|
662
739
|
|
|
663
740
|
if not self.linger:
|
|
664
741
|
self.done.set()
|
|
@@ -1471,7 +1548,7 @@ def create_mode_factory(ctx, default_mode):
|
|
|
1471
1548
|
def create_scenario_factory(ctx, default_scenario):
|
|
1472
1549
|
scenario = ctx.obj['scenario']
|
|
1473
1550
|
if scenario is None:
|
|
1474
|
-
|
|
1551
|
+
scenario = default_scenario
|
|
1475
1552
|
|
|
1476
1553
|
def create_scenario(packet_io):
|
|
1477
1554
|
if scenario == 'send':
|
|
@@ -1530,6 +1607,7 @@ def create_scenario_factory(ctx, default_scenario):
|
|
|
1530
1607
|
'--att-mtu',
|
|
1531
1608
|
metavar='MTU',
|
|
1532
1609
|
type=click.IntRange(23, 517),
|
|
1610
|
+
default=517,
|
|
1533
1611
|
help='GATT MTU (gatt-client mode)',
|
|
1534
1612
|
)
|
|
1535
1613
|
@click.option(
|
|
@@ -1605,7 +1683,7 @@ def create_scenario_factory(ctx, default_scenario):
|
|
|
1605
1683
|
'--packet-size',
|
|
1606
1684
|
'-s',
|
|
1607
1685
|
metavar='SIZE',
|
|
1608
|
-
type=click.IntRange(
|
|
1686
|
+
type=click.IntRange(10, 8192),
|
|
1609
1687
|
default=500,
|
|
1610
1688
|
help='Packet size (send or ping scenario)',
|
|
1611
1689
|
)
|
bumble/apps/controller_info.py
CHANGED
|
@@ -37,6 +37,8 @@ from bumble.hci import (
|
|
|
37
37
|
HCI_Command_Status_Event,
|
|
38
38
|
HCI_READ_BUFFER_SIZE_COMMAND,
|
|
39
39
|
HCI_Read_Buffer_Size_Command,
|
|
40
|
+
HCI_LE_READ_BUFFER_SIZE_V2_COMMAND,
|
|
41
|
+
HCI_LE_Read_Buffer_Size_V2_Command,
|
|
40
42
|
HCI_READ_BD_ADDR_COMMAND,
|
|
41
43
|
HCI_Read_BD_ADDR_Command,
|
|
42
44
|
HCI_READ_LOCAL_NAME_COMMAND,
|
|
@@ -75,7 +77,7 @@ async def get_classic_info(host: Host) -> None:
|
|
|
75
77
|
if command_succeeded(response):
|
|
76
78
|
print()
|
|
77
79
|
print(
|
|
78
|
-
color('
|
|
80
|
+
color('Public Address:', 'yellow'),
|
|
79
81
|
response.return_parameters.bd_addr.to_string(False),
|
|
80
82
|
)
|
|
81
83
|
|
|
@@ -147,7 +149,7 @@ async def get_le_info(host: Host) -> None:
|
|
|
147
149
|
|
|
148
150
|
|
|
149
151
|
# -----------------------------------------------------------------------------
|
|
150
|
-
async def
|
|
152
|
+
async def get_flow_control_info(host: Host) -> None:
|
|
151
153
|
print()
|
|
152
154
|
|
|
153
155
|
if host.supports_command(HCI_READ_BUFFER_SIZE_COMMAND):
|
|
@@ -160,14 +162,28 @@ async def get_acl_flow_control_info(host: Host) -> None:
|
|
|
160
162
|
f'packets of size {response.return_parameters.hc_acl_data_packet_length}',
|
|
161
163
|
)
|
|
162
164
|
|
|
163
|
-
if host.supports_command(
|
|
165
|
+
if host.supports_command(HCI_LE_READ_BUFFER_SIZE_V2_COMMAND):
|
|
166
|
+
response = await host.send_command(
|
|
167
|
+
HCI_LE_Read_Buffer_Size_V2_Command(), check_result=True
|
|
168
|
+
)
|
|
169
|
+
print(
|
|
170
|
+
color('LE ACL Flow Control:', 'yellow'),
|
|
171
|
+
f'{response.return_parameters.total_num_le_acl_data_packets} '
|
|
172
|
+
f'packets of size {response.return_parameters.le_acl_data_packet_length}',
|
|
173
|
+
)
|
|
174
|
+
print(
|
|
175
|
+
color('LE ISO Flow Control:', 'yellow'),
|
|
176
|
+
f'{response.return_parameters.total_num_iso_data_packets} '
|
|
177
|
+
f'packets of size {response.return_parameters.iso_data_packet_length}',
|
|
178
|
+
)
|
|
179
|
+
elif host.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
|
164
180
|
response = await host.send_command(
|
|
165
181
|
HCI_LE_Read_Buffer_Size_Command(), check_result=True
|
|
166
182
|
)
|
|
167
183
|
print(
|
|
168
184
|
color('LE ACL Flow Control:', 'yellow'),
|
|
169
|
-
f'{response.return_parameters.
|
|
170
|
-
f'packets of size {response.return_parameters.
|
|
185
|
+
f'{response.return_parameters.total_num_le_acl_data_packets} '
|
|
186
|
+
f'packets of size {response.return_parameters.le_acl_data_packet_length}',
|
|
171
187
|
)
|
|
172
188
|
|
|
173
189
|
|
|
@@ -274,8 +290,8 @@ async def async_main(latency_probes, transport):
|
|
|
274
290
|
# Get the LE info
|
|
275
291
|
await get_le_info(host)
|
|
276
292
|
|
|
277
|
-
# Print the
|
|
278
|
-
await
|
|
293
|
+
# Print the flow control info
|
|
294
|
+
await get_flow_control_info(host)
|
|
279
295
|
|
|
280
296
|
# Get codec info
|
|
281
297
|
await get_codecs_info(host)
|