bluetooth_bench.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. # Note: this test is for MacOS
  2. # The results are not very good between 2 MacBook Pro's:
  3. # INFO:__main__:Test 1: Write: 60.72 ms, Read: 59.08 ms, Total RTT: 119.80 ms, Timestamp: 1713, Time diff: 0 ms
  4. import asyncio
  5. import time
  6. import struct
  7. import argparse
  8. import logging
  9. from typing import Any
  10. # For the server
  11. from bless import BlessServer
  12. from bless.backends.characteristic import GATTCharacteristicProperties, GATTAttributePermissions
  13. # For the client
  14. from bleak import BleakClient, BleakScanner
  15. logging.basicConfig(level=logging.INFO)
  16. logger = logging.getLogger(__name__)
  17. SERVICE_UUID = "A07498CA-AD5B-474E-940D-16F1FBE7E8CD"
  18. CHAR_UUID = "51FF12BB-3ED8-46E5-B4F9-D64E2FEC021B"
  19. CONN_PARAMS_SERVICE_UUID = "1234A00C-0000-1000-8000-00805F9B34FB"
  20. CONN_PARAMS_CHAR_UUID = "1234A00D-0000-1000-8000-00805F9B34FB"
  21. # Define the desired connection interval (7.5ms)
  22. CONN_INTERVAL_MIN = 6 # 7.5ms in 1.25ms units
  23. CONN_INTERVAL_MAX = 6
  24. CONN_LATENCY = 0
  25. SUPERVISION_TIMEOUT = 100 # 1 second
  26. def read_request(characteristic):
  27. return characteristic.value
  28. def write_request(characteristic, value):
  29. characteristic.value = value
  30. if value == b"ping":
  31. characteristic.value = b"pong"
  32. async def run_server(loop):
  33. server = BlessServer(name="Latency Test Server", loop=loop)
  34. server.read_request_func = read_request
  35. server.write_request_func = write_request
  36. await server.add_new_service(SERVICE_UUID)
  37. # Main characteristic for ping-pong (read and write)
  38. char_flags = GATTCharacteristicProperties.read | GATTCharacteristicProperties.write
  39. permissions = GATTAttributePermissions.readable | GATTAttributePermissions.writeable
  40. await server.add_new_characteristic(
  41. SERVICE_UUID, CHAR_UUID, char_flags, None, permissions
  42. )
  43. # Add new service and characteristic for connection parameters (read-only)
  44. await server.add_new_service(CONN_PARAMS_SERVICE_UUID)
  45. conn_params = struct.pack("<HHHH", CONN_INTERVAL_MIN, CONN_INTERVAL_MAX, CONN_LATENCY, SUPERVISION_TIMEOUT)
  46. conn_params_flags = GATTCharacteristicProperties.read
  47. conn_params_permissions = GATTAttributePermissions.readable
  48. await server.add_new_characteristic(
  49. CONN_PARAMS_SERVICE_UUID, CONN_PARAMS_CHAR_UUID, conn_params_flags, conn_params, conn_params_permissions
  50. )
  51. await server.start()
  52. logger.info("Server started. Use the UUID of this device when running the client.")
  53. await asyncio.Event().wait() # Run forever
  54. async def run_client(server_uuid):
  55. logger.info(f"Connecting to server with UUID: {server_uuid}")
  56. async with BleakClient(server_uuid) as client:
  57. logger.info("Connected")
  58. # Read connection parameters
  59. try:
  60. conn_params = await client.read_gatt_char(CONN_PARAMS_CHAR_UUID)
  61. interval_min, interval_max, latency, timeout = struct.unpack("<HHHH", conn_params)
  62. logger.info(f"Connection parameters: Interval min: {interval_min * 1.25}ms, "
  63. f"Interval max: {interval_max * 1.25}ms, Latency: {latency}, "
  64. f"Timeout: {timeout * 10}ms")
  65. except Exception as e:
  66. logger.warning(f"Failed to read connection parameters: {e}")
  67. # Proceed with latency test
  68. num_tests = 50
  69. rtts = []
  70. last_timestamp = 0
  71. for i in range(num_tests):
  72. start_time = time.perf_counter()
  73. # Write operation
  74. await client.write_gatt_char(CHAR_UUID, b"ping")
  75. write_time = time.perf_counter()
  76. # Read operation
  77. response = await client.read_gatt_char(CHAR_UUID)
  78. end_time = time.perf_counter()
  79. write_latency = (write_time - start_time) * 1000
  80. read_latency = (end_time - write_time) * 1000
  81. total_rtt = (end_time - start_time) * 1000
  82. # Calculate timestamp (13-bit millisecond resolution as per BLE-MIDI spec)
  83. timestamp = int((start_time * 1000) % 8192)
  84. # Calculate time difference from last timestamp
  85. if last_timestamp:
  86. time_diff = (timestamp - last_timestamp) % 8192
  87. else:
  88. time_diff = 0
  89. last_timestamp = timestamp
  90. rtts.append(total_rtt)
  91. logger.info(f"Test {i+1}: Write: {write_latency:.2f} ms, Read: {read_latency:.2f} ms, "
  92. f"Total RTT: {total_rtt:.2f} ms, Timestamp: {timestamp}, Time diff: {time_diff} ms")
  93. await asyncio.sleep(0.01) # Small delay between tests
  94. average_rtt = sum(rtts) / num_tests
  95. median_rtt = sorted(rtts)[num_tests // 2]
  96. min_rtt = min(rtts)
  97. max_rtt = max(rtts)
  98. logger.info(f"\nAverage RTT: {average_rtt:.2f} ms")
  99. logger.info(f"Median RTT: {median_rtt:.2f} ms")
  100. logger.info(f"Min RTT: {min_rtt:.2f} ms")
  101. logger.info(f"Max RTT: {max_rtt:.2f} ms")
  102. async def discover_devices():
  103. logger.info("Scanning for BLE devices...")
  104. devices = await BleakScanner.discover()
  105. for d in devices:
  106. logger.info(f"Found device: {d.name} (UUID: {d.address})")
  107. return devices
  108. if __name__ == "__main__":
  109. parser = argparse.ArgumentParser(description="BLE Latency Test")
  110. parser.add_argument("mode", choices=["server", "client", "scan"], help="Run as server, client, or scan for devices")
  111. parser.add_argument("--uuid", help="Server's UUID (required for client mode)")
  112. args = parser.parse_args()
  113. if args.mode == "server":
  114. loop = asyncio.get_event_loop()
  115. loop.run_until_complete(run_server(loop))
  116. elif args.mode == "client":
  117. if not args.uuid:
  118. logger.error("Error: Server UUID is required for client mode.")
  119. exit(1)
  120. asyncio.run(run_client(args.uuid))
  121. elif args.mode == "scan":
  122. asyncio.run(discover_devices())