bumble 0.0.204__py3-none-any.whl → 0.0.208__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 +9 -4
- bumble/apps/auracast.py +631 -98
- bumble/apps/bench.py +238 -157
- bumble/apps/console.py +19 -12
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/gg_bridge.py +1 -1
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/att.py +51 -37
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +305 -156
- bumble/device.py +1090 -99
- bumble/gatt.py +36 -226
- bumble/gatt_adapters.py +374 -0
- bumble/gatt_client.py +52 -33
- bumble/gatt_server.py +5 -5
- bumble/hci.py +812 -14
- bumble/host.py +367 -65
- bumble/l2cap.py +3 -16
- bumble/pairing.py +5 -5
- bumble/pandora/host.py +7 -12
- bumble/profiles/aics.py +48 -57
- bumble/profiles/ascs.py +8 -19
- bumble/profiles/asha.py +16 -14
- bumble/profiles/bass.py +16 -22
- bumble/profiles/battery_service.py +13 -3
- bumble/profiles/device_information_service.py +16 -14
- bumble/profiles/gap.py +12 -8
- bumble/profiles/gatt_service.py +167 -0
- bumble/profiles/gmap.py +198 -0
- bumble/profiles/hap.py +8 -6
- bumble/profiles/heart_rate_service.py +20 -4
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/mcp.py +11 -9
- bumble/profiles/pacs.py +61 -16
- bumble/profiles/tmap.py +8 -12
- bumble/profiles/{vcp.py → vcs.py} +35 -29
- bumble/profiles/vocs.py +62 -85
- bumble/sdp.py +223 -93
- bumble/smp.py +1 -1
- bumble/utils.py +12 -2
- bumble/vendor/android/hci.py +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.208.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,49 +98,22 @@ 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)
|
|
131
104
|
)
|
|
132
105
|
|
|
133
106
|
|
|
107
|
+
def print_connection_phy(phy):
|
|
108
|
+
logging.info(
|
|
109
|
+
color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/'
|
|
110
|
+
f'RX:{le_phy_name(phy.rx_phy)}'
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
134
114
|
def print_connection(connection):
|
|
135
115
|
params = []
|
|
136
116
|
if connection.transport == BT_LE_TRANSPORT:
|
|
137
|
-
params.append(
|
|
138
|
-
'PHY='
|
|
139
|
-
f'TX:{le_phy_name(connection.phy.tx_phy)}/'
|
|
140
|
-
f'RX:{le_phy_name(connection.phy.rx_phy)}'
|
|
141
|
-
)
|
|
142
|
-
|
|
143
117
|
params.append(
|
|
144
118
|
'DL=('
|
|
145
119
|
f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
|
|
@@ -225,13 +199,135 @@ async def switch_roles(connection, role):
|
|
|
225
199
|
logging.info(f'{color("### Role switch failed:", "red")} {error}')
|
|
226
200
|
|
|
227
201
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
202
|
+
# -----------------------------------------------------------------------------
|
|
203
|
+
# Packet
|
|
204
|
+
# -----------------------------------------------------------------------------
|
|
205
|
+
@dataclasses.dataclass
|
|
206
|
+
class Packet:
|
|
207
|
+
class PacketType(enum.IntEnum):
|
|
208
|
+
RESET = 0
|
|
209
|
+
SEQUENCE = 1
|
|
210
|
+
ACK = 2
|
|
211
|
+
|
|
212
|
+
class PacketFlags(enum.IntFlag):
|
|
213
|
+
LAST = 1
|
|
214
|
+
|
|
215
|
+
packet_type: PacketType
|
|
216
|
+
flags: PacketFlags = PacketFlags(0)
|
|
217
|
+
sequence: int = 0
|
|
218
|
+
timestamp: int = 0
|
|
219
|
+
payload: bytes = b""
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def from_bytes(cls, data: bytes):
|
|
223
|
+
if len(data) < 1:
|
|
224
|
+
logging.warning(
|
|
225
|
+
color(f'!!! Packet too short (got {len(data)} bytes, need >= 1)', 'red')
|
|
226
|
+
)
|
|
227
|
+
raise ValueError('packet too short')
|
|
232
228
|
|
|
229
|
+
try:
|
|
230
|
+
packet_type = cls.PacketType(data[0])
|
|
231
|
+
except ValueError:
|
|
232
|
+
logging.warning(color(f'!!! Invalid packet type 0x{data[0]:02X}', 'red'))
|
|
233
|
+
raise
|
|
233
234
|
|
|
234
|
-
|
|
235
|
+
if packet_type == cls.PacketType.RESET:
|
|
236
|
+
return cls(packet_type)
|
|
237
|
+
|
|
238
|
+
flags = cls.PacketFlags(data[1])
|
|
239
|
+
(sequence,) = struct.unpack_from("<I", data, 2)
|
|
240
|
+
|
|
241
|
+
if packet_type == cls.PacketType.ACK:
|
|
242
|
+
if len(data) < 6:
|
|
243
|
+
logging.warning(
|
|
244
|
+
color(
|
|
245
|
+
f'!!! Packet too short (got {len(data)} bytes, need >= 6)',
|
|
246
|
+
'red',
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
return cls(packet_type, flags, sequence)
|
|
250
|
+
|
|
251
|
+
if len(data) < 10:
|
|
252
|
+
logging.warning(
|
|
253
|
+
color(
|
|
254
|
+
f'!!! Packet too short (got {len(data)} bytes, need >= 10)', 'red'
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
raise ValueError('packet too short')
|
|
258
|
+
|
|
259
|
+
(timestamp,) = struct.unpack_from("<I", data, 6)
|
|
260
|
+
return cls(packet_type, flags, sequence, timestamp, data[10:])
|
|
261
|
+
|
|
262
|
+
def __bytes__(self):
|
|
263
|
+
if self.packet_type == self.PacketType.RESET:
|
|
264
|
+
return bytes([self.packet_type])
|
|
265
|
+
|
|
266
|
+
if self.packet_type == self.PacketType.ACK:
|
|
267
|
+
return struct.pack("<BBI", self.packet_type, self.flags, self.sequence)
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
struct.pack(
|
|
271
|
+
"<BBII", self.packet_type, self.flags, self.sequence, self.timestamp
|
|
272
|
+
)
|
|
273
|
+
+ self.payload
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# -----------------------------------------------------------------------------
|
|
278
|
+
# Jitter Stats
|
|
279
|
+
# -----------------------------------------------------------------------------
|
|
280
|
+
class JitterStats:
|
|
281
|
+
def __init__(self):
|
|
282
|
+
self.reset()
|
|
283
|
+
|
|
284
|
+
def reset(self):
|
|
285
|
+
self.packets = []
|
|
286
|
+
self.receive_times = []
|
|
287
|
+
self.jitter = []
|
|
288
|
+
|
|
289
|
+
def on_packet_received(self, packet):
|
|
290
|
+
now = time.time()
|
|
291
|
+
self.packets.append(packet)
|
|
292
|
+
self.receive_times.append(now)
|
|
293
|
+
|
|
294
|
+
if packet.timestamp and len(self.packets) > 1:
|
|
295
|
+
expected_time = (
|
|
296
|
+
self.receive_times[0]
|
|
297
|
+
+ (packet.timestamp - self.packets[0].timestamp) / 1000000
|
|
298
|
+
)
|
|
299
|
+
jitter = now - expected_time
|
|
300
|
+
else:
|
|
301
|
+
jitter = 0.0
|
|
302
|
+
|
|
303
|
+
self.jitter.append(jitter)
|
|
304
|
+
return jitter
|
|
305
|
+
|
|
306
|
+
def show_stats(self):
|
|
307
|
+
if len(self.jitter) < 3:
|
|
308
|
+
return
|
|
309
|
+
average = sum(self.jitter) / len(self.jitter)
|
|
310
|
+
adjusted = [jitter - average for jitter in self.jitter]
|
|
311
|
+
|
|
312
|
+
log_stats('Jitter (signed)', adjusted, 3)
|
|
313
|
+
log_stats('Jitter (absolute)', [abs(jitter) for jitter in adjusted], 3)
|
|
314
|
+
|
|
315
|
+
# Show a histogram
|
|
316
|
+
bin_count = 20
|
|
317
|
+
bins = [0] * bin_count
|
|
318
|
+
interval_min = min(adjusted)
|
|
319
|
+
interval_max = max(adjusted)
|
|
320
|
+
interval_range = interval_max - interval_min
|
|
321
|
+
bin_thresholds = [
|
|
322
|
+
interval_min + i * (interval_range / bin_count) for i in range(bin_count)
|
|
323
|
+
]
|
|
324
|
+
for jitter in adjusted:
|
|
325
|
+
for i in reversed(range(bin_count)):
|
|
326
|
+
if jitter >= bin_thresholds[i]:
|
|
327
|
+
bins[i] += 1
|
|
328
|
+
break
|
|
329
|
+
for i in range(bin_count):
|
|
330
|
+
logging.info(f'@@@ >= {bin_thresholds[i]:.4f}: {bins[i]}')
|
|
235
331
|
|
|
236
332
|
|
|
237
333
|
# -----------------------------------------------------------------------------
|
|
@@ -281,19 +377,37 @@ class Sender:
|
|
|
281
377
|
await asyncio.sleep(self.tx_start_delay)
|
|
282
378
|
|
|
283
379
|
logging.info(color('=== Sending RESET', 'magenta'))
|
|
284
|
-
await self.packet_io.send_packet(
|
|
380
|
+
await self.packet_io.send_packet(
|
|
381
|
+
bytes(Packet(packet_type=Packet.PacketType.RESET))
|
|
382
|
+
)
|
|
383
|
+
|
|
285
384
|
self.start_time = time.time()
|
|
286
385
|
self.bytes_sent = 0
|
|
287
386
|
for tx_i in range(self.tx_packet_count):
|
|
288
|
-
|
|
289
|
-
|
|
387
|
+
if self.pace > 0:
|
|
388
|
+
# Wait until it is time to send the next packet
|
|
389
|
+
target_time = self.start_time + (tx_i * self.pace / 1000)
|
|
390
|
+
now = time.time()
|
|
391
|
+
if now < target_time:
|
|
392
|
+
await asyncio.sleep(target_time - now)
|
|
393
|
+
else:
|
|
394
|
+
await self.packet_io.drain()
|
|
395
|
+
|
|
396
|
+
packet = bytes(
|
|
397
|
+
Packet(
|
|
398
|
+
packet_type=Packet.PacketType.SEQUENCE,
|
|
399
|
+
flags=(
|
|
400
|
+
Packet.PacketFlags.LAST
|
|
401
|
+
if tx_i == self.tx_packet_count - 1
|
|
402
|
+
else 0
|
|
403
|
+
),
|
|
404
|
+
sequence=tx_i,
|
|
405
|
+
timestamp=int((time.time() - self.start_time) * 1000000),
|
|
406
|
+
payload=bytes(
|
|
407
|
+
self.tx_packet_size - 10 - self.packet_io.overhead_size
|
|
408
|
+
),
|
|
409
|
+
)
|
|
290
410
|
)
|
|
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
411
|
logging.info(
|
|
298
412
|
color(
|
|
299
413
|
f'Sending packet {tx_i}: {self.tx_packet_size} bytes', 'yellow'
|
|
@@ -302,14 +416,6 @@ class Sender:
|
|
|
302
416
|
self.bytes_sent += len(packet)
|
|
303
417
|
await self.packet_io.send_packet(packet)
|
|
304
418
|
|
|
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
419
|
await self.done.wait()
|
|
314
420
|
|
|
315
421
|
run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
|
|
@@ -321,13 +427,13 @@ class Sender:
|
|
|
321
427
|
if self.repeat:
|
|
322
428
|
logging.info(color('--- End of runs', 'blue'))
|
|
323
429
|
|
|
324
|
-
def on_packet_received(self,
|
|
430
|
+
def on_packet_received(self, data):
|
|
325
431
|
try:
|
|
326
|
-
|
|
432
|
+
packet = Packet.from_bytes(data)
|
|
327
433
|
except ValueError:
|
|
328
434
|
return
|
|
329
435
|
|
|
330
|
-
if packet_type == PacketType.ACK:
|
|
436
|
+
if packet.packet_type == Packet.PacketType.ACK:
|
|
331
437
|
elapsed = time.time() - self.start_time
|
|
332
438
|
average_tx_speed = self.bytes_sent / elapsed
|
|
333
439
|
self.stats.append(average_tx_speed)
|
|
@@ -350,52 +456,53 @@ class Receiver:
|
|
|
350
456
|
last_timestamp: float
|
|
351
457
|
|
|
352
458
|
def __init__(self, packet_io, linger):
|
|
353
|
-
self.
|
|
459
|
+
self.jitter_stats = JitterStats()
|
|
354
460
|
self.packet_io = packet_io
|
|
355
461
|
self.packet_io.packet_listener = self
|
|
356
462
|
self.linger = linger
|
|
357
463
|
self.done = asyncio.Event()
|
|
464
|
+
self.reset()
|
|
358
465
|
|
|
359
466
|
def reset(self):
|
|
360
467
|
self.expected_packet_index = 0
|
|
361
468
|
self.measurements = [(time.time(), 0)]
|
|
362
469
|
self.total_bytes_received = 0
|
|
470
|
+
self.jitter_stats.reset()
|
|
363
471
|
|
|
364
|
-
def on_packet_received(self,
|
|
472
|
+
def on_packet_received(self, data):
|
|
365
473
|
try:
|
|
366
|
-
|
|
474
|
+
packet = Packet.from_bytes(data)
|
|
367
475
|
except ValueError:
|
|
476
|
+
logging.exception("invalid packet")
|
|
368
477
|
return
|
|
369
478
|
|
|
370
|
-
if packet_type == PacketType.RESET:
|
|
479
|
+
if packet.packet_type == Packet.PacketType.RESET:
|
|
371
480
|
logging.info(color('=== Received RESET', 'magenta'))
|
|
372
481
|
self.reset()
|
|
373
482
|
return
|
|
374
483
|
|
|
375
|
-
|
|
376
|
-
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
|
377
|
-
except ValueError:
|
|
378
|
-
return
|
|
484
|
+
jitter = self.jitter_stats.on_packet_received(packet)
|
|
379
485
|
logging.info(
|
|
380
|
-
f'<<< Received packet {
|
|
381
|
-
f'flags=
|
|
382
|
-
f'{
|
|
486
|
+
f'<<< Received packet {packet.sequence}: '
|
|
487
|
+
f'flags={packet.flags}, '
|
|
488
|
+
f'jitter={jitter:.4f}, '
|
|
489
|
+
f'{len(data) + self.packet_io.overhead_size} bytes',
|
|
383
490
|
)
|
|
384
491
|
|
|
385
|
-
if
|
|
492
|
+
if packet.sequence != self.expected_packet_index:
|
|
386
493
|
logging.info(
|
|
387
494
|
color(
|
|
388
495
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
|
389
|
-
f'but received {
|
|
496
|
+
f'but received {packet.sequence}'
|
|
390
497
|
)
|
|
391
498
|
)
|
|
392
499
|
|
|
393
500
|
now = time.time()
|
|
394
501
|
elapsed_since_start = now - self.measurements[0][0]
|
|
395
502
|
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(
|
|
503
|
+
self.measurements.append((now, len(data)))
|
|
504
|
+
self.total_bytes_received += len(data)
|
|
505
|
+
instant_rx_speed = len(data) / elapsed_since_last
|
|
399
506
|
average_rx_speed = self.total_bytes_received / elapsed_since_start
|
|
400
507
|
window = self.measurements[-64:]
|
|
401
508
|
windowed_rx_speed = sum(measurement[1] for measurement in window[1:]) / (
|
|
@@ -411,15 +518,17 @@ class Receiver:
|
|
|
411
518
|
)
|
|
412
519
|
)
|
|
413
520
|
|
|
414
|
-
self.expected_packet_index =
|
|
521
|
+
self.expected_packet_index = packet.sequence + 1
|
|
415
522
|
|
|
416
|
-
if
|
|
523
|
+
if packet.flags & Packet.PacketFlags.LAST:
|
|
417
524
|
AsyncRunner.spawn(
|
|
418
525
|
self.packet_io.send_packet(
|
|
419
|
-
|
|
526
|
+
bytes(Packet(Packet.PacketType.ACK, packet.flags, packet.sequence))
|
|
420
527
|
)
|
|
421
528
|
)
|
|
422
529
|
logging.info(color('@@@ Received last packet', 'green'))
|
|
530
|
+
self.jitter_stats.show_stats()
|
|
531
|
+
|
|
423
532
|
if not self.linger:
|
|
424
533
|
self.done.set()
|
|
425
534
|
|
|
@@ -479,25 +588,32 @@ class Ping:
|
|
|
479
588
|
await asyncio.sleep(self.tx_start_delay)
|
|
480
589
|
|
|
481
590
|
logging.info(color('=== Sending RESET', 'magenta'))
|
|
482
|
-
await self.packet_io.send_packet(bytes(
|
|
591
|
+
await self.packet_io.send_packet(bytes(Packet(Packet.PacketType.RESET)))
|
|
483
592
|
|
|
484
|
-
packet_interval = self.pace / 1000
|
|
485
593
|
start_time = time.time()
|
|
486
594
|
self.next_expected_packet_index = 0
|
|
487
595
|
for i in range(self.tx_packet_count):
|
|
488
|
-
target_time = start_time + (i *
|
|
596
|
+
target_time = start_time + (i * self.pace / 1000)
|
|
489
597
|
now = time.time()
|
|
490
598
|
if now < target_time:
|
|
491
599
|
await asyncio.sleep(target_time - now)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
600
|
+
now = time.time()
|
|
601
|
+
|
|
602
|
+
packet = bytes(
|
|
603
|
+
Packet(
|
|
604
|
+
packet_type=Packet.PacketType.SEQUENCE,
|
|
605
|
+
flags=(
|
|
606
|
+
Packet.PacketFlags.LAST
|
|
607
|
+
if i == self.tx_packet_count - 1
|
|
608
|
+
else 0
|
|
609
|
+
),
|
|
610
|
+
sequence=i,
|
|
611
|
+
timestamp=int((now - start_time) * 1000000),
|
|
612
|
+
payload=bytes(self.tx_packet_size - 10),
|
|
613
|
+
)
|
|
614
|
+
)
|
|
499
615
|
logging.info(color(f'Sending packet {i}', 'yellow'))
|
|
500
|
-
self.ping_times.append(
|
|
616
|
+
self.ping_times.append(now)
|
|
501
617
|
await self.packet_io.send_packet(packet)
|
|
502
618
|
|
|
503
619
|
await self.done.wait()
|
|
@@ -531,40 +647,35 @@ class Ping:
|
|
|
531
647
|
if self.repeat:
|
|
532
648
|
logging.info(color('--- End of runs', 'blue'))
|
|
533
649
|
|
|
534
|
-
def on_packet_received(self,
|
|
650
|
+
def on_packet_received(self, data):
|
|
535
651
|
try:
|
|
536
|
-
|
|
652
|
+
packet = Packet.from_bytes(data)
|
|
537
653
|
except ValueError:
|
|
538
654
|
return
|
|
539
655
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
except ValueError:
|
|
543
|
-
return
|
|
544
|
-
|
|
545
|
-
if packet_type == PacketType.ACK:
|
|
546
|
-
elapsed = time.time() - self.ping_times[packet_index]
|
|
656
|
+
if packet.packet_type == Packet.PacketType.ACK:
|
|
657
|
+
elapsed = time.time() - self.ping_times[packet.sequence]
|
|
547
658
|
rtt = elapsed * 1000
|
|
548
659
|
self.rtts.append(rtt)
|
|
549
660
|
logging.info(
|
|
550
661
|
color(
|
|
551
|
-
f'<<< Received ACK [{
|
|
662
|
+
f'<<< Received ACK [{packet.sequence}], RTT={rtt:.2f}ms',
|
|
552
663
|
'green',
|
|
553
664
|
)
|
|
554
665
|
)
|
|
555
666
|
|
|
556
|
-
if
|
|
667
|
+
if packet.sequence == self.next_expected_packet_index:
|
|
557
668
|
self.next_expected_packet_index += 1
|
|
558
669
|
else:
|
|
559
670
|
logging.info(
|
|
560
671
|
color(
|
|
561
672
|
f'!!! Unexpected packet, '
|
|
562
673
|
f'expected {self.next_expected_packet_index} '
|
|
563
|
-
f'but received {
|
|
674
|
+
f'but received {packet.sequence}'
|
|
564
675
|
)
|
|
565
676
|
)
|
|
566
677
|
|
|
567
|
-
if
|
|
678
|
+
if packet.flags & Packet.PacketFlags.LAST:
|
|
568
679
|
self.done.set()
|
|
569
680
|
return
|
|
570
681
|
|
|
@@ -576,89 +687,56 @@ class Pong:
|
|
|
576
687
|
expected_packet_index: int
|
|
577
688
|
|
|
578
689
|
def __init__(self, packet_io, linger):
|
|
579
|
-
self.
|
|
690
|
+
self.jitter_stats = JitterStats()
|
|
580
691
|
self.packet_io = packet_io
|
|
581
692
|
self.packet_io.packet_listener = self
|
|
582
693
|
self.linger = linger
|
|
583
694
|
self.done = asyncio.Event()
|
|
695
|
+
self.reset()
|
|
584
696
|
|
|
585
697
|
def reset(self):
|
|
586
698
|
self.expected_packet_index = 0
|
|
587
|
-
self.
|
|
588
|
-
|
|
589
|
-
def on_packet_received(self, packet):
|
|
590
|
-
self.receive_times.append(time.time())
|
|
699
|
+
self.jitter_stats.reset()
|
|
591
700
|
|
|
701
|
+
def on_packet_received(self, data):
|
|
592
702
|
try:
|
|
593
|
-
|
|
703
|
+
packet = Packet.from_bytes(data)
|
|
594
704
|
except ValueError:
|
|
595
705
|
return
|
|
596
706
|
|
|
597
|
-
if packet_type == PacketType.RESET:
|
|
707
|
+
if packet.packet_type == Packet.PacketType.RESET:
|
|
598
708
|
logging.info(color('=== Received RESET', 'magenta'))
|
|
599
709
|
self.reset()
|
|
600
710
|
return
|
|
601
711
|
|
|
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
|
-
)
|
|
712
|
+
jitter = self.jitter_stats.on_packet_received(packet)
|
|
611
713
|
logging.info(
|
|
612
714
|
color(
|
|
613
|
-
f'<<< Received packet {
|
|
614
|
-
f'flags=
|
|
615
|
-
f'
|
|
715
|
+
f'<<< Received packet {packet.sequence}: '
|
|
716
|
+
f'flags={packet.flags}, {len(data)} bytes, '
|
|
717
|
+
f'jitter={jitter:.4f}',
|
|
616
718
|
'green',
|
|
617
719
|
)
|
|
618
720
|
)
|
|
619
721
|
|
|
620
|
-
if
|
|
722
|
+
if packet.sequence != self.expected_packet_index:
|
|
621
723
|
logging.info(
|
|
622
724
|
color(
|
|
623
725
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
|
624
|
-
f'but received {
|
|
726
|
+
f'but received {packet.sequence}'
|
|
625
727
|
)
|
|
626
728
|
)
|
|
627
729
|
|
|
628
|
-
self.expected_packet_index =
|
|
730
|
+
self.expected_packet_index = packet.sequence + 1
|
|
629
731
|
|
|
630
732
|
AsyncRunner.spawn(
|
|
631
733
|
self.packet_io.send_packet(
|
|
632
|
-
|
|
734
|
+
bytes(Packet(Packet.PacketType.ACK, packet.flags, packet.sequence))
|
|
633
735
|
)
|
|
634
736
|
)
|
|
635
737
|
|
|
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]}')
|
|
738
|
+
if packet.flags & Packet.PacketFlags.LAST:
|
|
739
|
+
self.jitter_stats.show_stats()
|
|
662
740
|
|
|
663
741
|
if not self.linger:
|
|
664
742
|
self.done.set()
|
|
@@ -1211,6 +1289,8 @@ class Central(Connection.Listener):
|
|
|
1211
1289
|
logging.info(color('### Connected', 'cyan'))
|
|
1212
1290
|
self.connection.listener = self
|
|
1213
1291
|
print_connection(self.connection)
|
|
1292
|
+
phy = await self.connection.get_phy()
|
|
1293
|
+
print_connection_phy(phy)
|
|
1214
1294
|
|
|
1215
1295
|
# Switch roles if needed.
|
|
1216
1296
|
if self.role_switch:
|
|
@@ -1268,8 +1348,8 @@ class Central(Connection.Listener):
|
|
|
1268
1348
|
def on_connection_parameters_update(self):
|
|
1269
1349
|
print_connection(self.connection)
|
|
1270
1350
|
|
|
1271
|
-
def on_connection_phy_update(self):
|
|
1272
|
-
|
|
1351
|
+
def on_connection_phy_update(self, phy):
|
|
1352
|
+
print_connection_phy(phy)
|
|
1273
1353
|
|
|
1274
1354
|
def on_connection_att_mtu_update(self):
|
|
1275
1355
|
print_connection(self.connection)
|
|
@@ -1395,8 +1475,8 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1395
1475
|
def on_connection_parameters_update(self):
|
|
1396
1476
|
print_connection(self.connection)
|
|
1397
1477
|
|
|
1398
|
-
def on_connection_phy_update(self):
|
|
1399
|
-
|
|
1478
|
+
def on_connection_phy_update(self, phy):
|
|
1479
|
+
print_connection_phy(phy)
|
|
1400
1480
|
|
|
1401
1481
|
def on_connection_att_mtu_update(self):
|
|
1402
1482
|
print_connection(self.connection)
|
|
@@ -1471,7 +1551,7 @@ def create_mode_factory(ctx, default_mode):
|
|
|
1471
1551
|
def create_scenario_factory(ctx, default_scenario):
|
|
1472
1552
|
scenario = ctx.obj['scenario']
|
|
1473
1553
|
if scenario is None:
|
|
1474
|
-
|
|
1554
|
+
scenario = default_scenario
|
|
1475
1555
|
|
|
1476
1556
|
def create_scenario(packet_io):
|
|
1477
1557
|
if scenario == 'send':
|
|
@@ -1530,6 +1610,7 @@ def create_scenario_factory(ctx, default_scenario):
|
|
|
1530
1610
|
'--att-mtu',
|
|
1531
1611
|
metavar='MTU',
|
|
1532
1612
|
type=click.IntRange(23, 517),
|
|
1613
|
+
default=517,
|
|
1533
1614
|
help='GATT MTU (gatt-client mode)',
|
|
1534
1615
|
)
|
|
1535
1616
|
@click.option(
|
|
@@ -1605,7 +1686,7 @@ def create_scenario_factory(ctx, default_scenario):
|
|
|
1605
1686
|
'--packet-size',
|
|
1606
1687
|
'-s',
|
|
1607
1688
|
metavar='SIZE',
|
|
1608
|
-
type=click.IntRange(
|
|
1689
|
+
type=click.IntRange(10, 8192),
|
|
1609
1690
|
default=500,
|
|
1610
1691
|
help='Packet size (send or ping scenario)',
|
|
1611
1692
|
)
|