bumble 0.0.203__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 +227 -148
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/apps/pair.py +13 -8
- bumble/apps/show.py +6 -6
- bumble/att.py +10 -11
- 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/drivers/common.py +2 -0
- bumble/drivers/intel.py +593 -24
- bumble/gatt.py +67 -12
- bumble/gatt_client.py +14 -2
- bumble/gatt_server.py +12 -1
- bumble/hci.py +854 -33
- bumble/host.py +363 -64
- bumble/l2cap.py +3 -16
- bumble/pairing.py +3 -0
- bumble/profiles/aics.py +45 -80
- bumble/profiles/ascs.py +6 -18
- bumble/profiles/asha.py +5 -5
- bumble/profiles/bass.py +9 -21
- bumble/profiles/device_information_service.py +4 -1
- bumble/profiles/gatt_service.py +166 -0
- bumble/profiles/gmap.py +193 -0
- bumble/profiles/heart_rate_service.py +5 -6
- 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 +299 -0
- bumble/sdp.py +223 -93
- bumble/smp.py +8 -3
- bumble/tools/intel_fw_download.py +130 -0
- bumble/tools/intel_util.py +154 -0
- bumble/transport/usb.py +8 -2
- bumble/utils.py +22 -7
- bumble/vendor/android/hci.py +29 -4
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/METADATA +12 -10
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/RECORD +49 -43
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/WHEEL +1 -1
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/entry_points.txt +3 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/LICENSE +0 -0
- {bumble-0.0.203.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)
|
|
@@ -199,7 +172,7 @@ def log_stats(title, stats, precision=2):
|
|
|
199
172
|
stats_min = min(stats)
|
|
200
173
|
stats_max = max(stats)
|
|
201
174
|
stats_avg = statistics.mean(stats)
|
|
202
|
-
stats_stdev = statistics.stdev(stats)
|
|
175
|
+
stats_stdev = statistics.stdev(stats) if len(stats) >= 2 else 0
|
|
203
176
|
logging.info(
|
|
204
177
|
color(
|
|
205
178
|
(
|
|
@@ -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
|
|
|
@@ -468,6 +576,7 @@ class Ping:
|
|
|
468
576
|
|
|
469
577
|
for run in range(self.repeat + 1):
|
|
470
578
|
self.done.clear()
|
|
579
|
+
self.ping_times = []
|
|
471
580
|
|
|
472
581
|
if run > 0 and self.repeat and self.repeat_delay:
|
|
473
582
|
logging.info(color(f'*** Repeat delay: {self.repeat_delay}', 'green'))
|
|
@@ -478,25 +587,32 @@ class Ping:
|
|
|
478
587
|
await asyncio.sleep(self.tx_start_delay)
|
|
479
588
|
|
|
480
589
|
logging.info(color('=== Sending RESET', 'magenta'))
|
|
481
|
-
await self.packet_io.send_packet(bytes(
|
|
590
|
+
await self.packet_io.send_packet(bytes(Packet(Packet.PacketType.RESET)))
|
|
482
591
|
|
|
483
|
-
packet_interval = self.pace / 1000
|
|
484
592
|
start_time = time.time()
|
|
485
593
|
self.next_expected_packet_index = 0
|
|
486
594
|
for i in range(self.tx_packet_count):
|
|
487
|
-
target_time = start_time + (i *
|
|
595
|
+
target_time = start_time + (i * self.pace / 1000)
|
|
488
596
|
now = time.time()
|
|
489
597
|
if now < target_time:
|
|
490
598
|
await asyncio.sleep(target_time - now)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
)
|
|
498
614
|
logging.info(color(f'Sending packet {i}', 'yellow'))
|
|
499
|
-
self.ping_times.append(
|
|
615
|
+
self.ping_times.append(now)
|
|
500
616
|
await self.packet_io.send_packet(packet)
|
|
501
617
|
|
|
502
618
|
await self.done.wait()
|
|
@@ -530,40 +646,35 @@ class Ping:
|
|
|
530
646
|
if self.repeat:
|
|
531
647
|
logging.info(color('--- End of runs', 'blue'))
|
|
532
648
|
|
|
533
|
-
def on_packet_received(self,
|
|
649
|
+
def on_packet_received(self, data):
|
|
534
650
|
try:
|
|
535
|
-
|
|
651
|
+
packet = Packet.from_bytes(data)
|
|
536
652
|
except ValueError:
|
|
537
653
|
return
|
|
538
654
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
except ValueError:
|
|
542
|
-
return
|
|
543
|
-
|
|
544
|
-
if packet_type == PacketType.ACK:
|
|
545
|
-
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]
|
|
546
657
|
rtt = elapsed * 1000
|
|
547
658
|
self.rtts.append(rtt)
|
|
548
659
|
logging.info(
|
|
549
660
|
color(
|
|
550
|
-
f'<<< Received ACK [{
|
|
661
|
+
f'<<< Received ACK [{packet.sequence}], RTT={rtt:.2f}ms',
|
|
551
662
|
'green',
|
|
552
663
|
)
|
|
553
664
|
)
|
|
554
665
|
|
|
555
|
-
if
|
|
666
|
+
if packet.sequence == self.next_expected_packet_index:
|
|
556
667
|
self.next_expected_packet_index += 1
|
|
557
668
|
else:
|
|
558
669
|
logging.info(
|
|
559
670
|
color(
|
|
560
671
|
f'!!! Unexpected packet, '
|
|
561
672
|
f'expected {self.next_expected_packet_index} '
|
|
562
|
-
f'but received {
|
|
673
|
+
f'but received {packet.sequence}'
|
|
563
674
|
)
|
|
564
675
|
)
|
|
565
676
|
|
|
566
|
-
if
|
|
677
|
+
if packet.flags & Packet.PacketFlags.LAST:
|
|
567
678
|
self.done.set()
|
|
568
679
|
return
|
|
569
680
|
|
|
@@ -575,89 +686,56 @@ class Pong:
|
|
|
575
686
|
expected_packet_index: int
|
|
576
687
|
|
|
577
688
|
def __init__(self, packet_io, linger):
|
|
578
|
-
self.
|
|
689
|
+
self.jitter_stats = JitterStats()
|
|
579
690
|
self.packet_io = packet_io
|
|
580
691
|
self.packet_io.packet_listener = self
|
|
581
692
|
self.linger = linger
|
|
582
693
|
self.done = asyncio.Event()
|
|
694
|
+
self.reset()
|
|
583
695
|
|
|
584
696
|
def reset(self):
|
|
585
697
|
self.expected_packet_index = 0
|
|
586
|
-
self.
|
|
587
|
-
|
|
588
|
-
def on_packet_received(self, packet):
|
|
589
|
-
self.receive_times.append(time.time())
|
|
698
|
+
self.jitter_stats.reset()
|
|
590
699
|
|
|
700
|
+
def on_packet_received(self, data):
|
|
591
701
|
try:
|
|
592
|
-
|
|
702
|
+
packet = Packet.from_bytes(data)
|
|
593
703
|
except ValueError:
|
|
594
704
|
return
|
|
595
705
|
|
|
596
|
-
if packet_type == PacketType.RESET:
|
|
706
|
+
if packet.packet_type == Packet.PacketType.RESET:
|
|
597
707
|
logging.info(color('=== Received RESET', 'magenta'))
|
|
598
708
|
self.reset()
|
|
599
709
|
return
|
|
600
710
|
|
|
601
|
-
|
|
602
|
-
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
|
603
|
-
except ValueError:
|
|
604
|
-
return
|
|
605
|
-
interval = (
|
|
606
|
-
self.receive_times[-1] - self.receive_times[-2]
|
|
607
|
-
if len(self.receive_times) >= 2
|
|
608
|
-
else 0
|
|
609
|
-
)
|
|
711
|
+
jitter = self.jitter_stats.on_packet_received(packet)
|
|
610
712
|
logging.info(
|
|
611
713
|
color(
|
|
612
|
-
f'<<< Received packet {
|
|
613
|
-
f'flags=
|
|
614
|
-
f'
|
|
714
|
+
f'<<< Received packet {packet.sequence}: '
|
|
715
|
+
f'flags={packet.flags}, {len(data)} bytes, '
|
|
716
|
+
f'jitter={jitter:.4f}',
|
|
615
717
|
'green',
|
|
616
718
|
)
|
|
617
719
|
)
|
|
618
720
|
|
|
619
|
-
if
|
|
721
|
+
if packet.sequence != self.expected_packet_index:
|
|
620
722
|
logging.info(
|
|
621
723
|
color(
|
|
622
724
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
|
623
|
-
f'but received {
|
|
725
|
+
f'but received {packet.sequence}'
|
|
624
726
|
)
|
|
625
727
|
)
|
|
626
728
|
|
|
627
|
-
self.expected_packet_index =
|
|
729
|
+
self.expected_packet_index = packet.sequence + 1
|
|
628
730
|
|
|
629
731
|
AsyncRunner.spawn(
|
|
630
732
|
self.packet_io.send_packet(
|
|
631
|
-
|
|
733
|
+
bytes(Packet(Packet.PacketType.ACK, packet.flags, packet.sequence))
|
|
632
734
|
)
|
|
633
735
|
)
|
|
634
736
|
|
|
635
|
-
if
|
|
636
|
-
|
|
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]}')
|
|
737
|
+
if packet.flags & Packet.PacketFlags.LAST:
|
|
738
|
+
self.jitter_stats.show_stats()
|
|
661
739
|
|
|
662
740
|
if not self.linger:
|
|
663
741
|
self.done.set()
|
|
@@ -1470,7 +1548,7 @@ def create_mode_factory(ctx, default_mode):
|
|
|
1470
1548
|
def create_scenario_factory(ctx, default_scenario):
|
|
1471
1549
|
scenario = ctx.obj['scenario']
|
|
1472
1550
|
if scenario is None:
|
|
1473
|
-
|
|
1551
|
+
scenario = default_scenario
|
|
1474
1552
|
|
|
1475
1553
|
def create_scenario(packet_io):
|
|
1476
1554
|
if scenario == 'send':
|
|
@@ -1529,6 +1607,7 @@ def create_scenario_factory(ctx, default_scenario):
|
|
|
1529
1607
|
'--att-mtu',
|
|
1530
1608
|
metavar='MTU',
|
|
1531
1609
|
type=click.IntRange(23, 517),
|
|
1610
|
+
default=517,
|
|
1532
1611
|
help='GATT MTU (gatt-client mode)',
|
|
1533
1612
|
)
|
|
1534
1613
|
@click.option(
|
|
@@ -1604,7 +1683,7 @@ def create_scenario_factory(ctx, default_scenario):
|
|
|
1604
1683
|
'--packet-size',
|
|
1605
1684
|
'-s',
|
|
1606
1685
|
metavar='SIZE',
|
|
1607
|
-
type=click.IntRange(
|
|
1686
|
+
type=click.IntRange(10, 8192),
|
|
1608
1687
|
default=500,
|
|
1609
1688
|
help='Packet size (send or ping scenario)',
|
|
1610
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)
|