peripheral.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. # MicroPython aioble module
  2. # MIT license; Copyright (c) 2021 Jim Mussared
  3. from micropython import const
  4. import bluetooth
  5. import struct
  6. import asyncio
  7. from .core import (
  8. ensure_active,
  9. ble,
  10. log_info,
  11. log_error,
  12. log_warn,
  13. register_irq_handler,
  14. )
  15. from .device import Device, DeviceConnection, DeviceTimeout
  16. _IRQ_CENTRAL_CONNECT = const(1)
  17. _IRQ_CENTRAL_DISCONNECT = const(2)
  18. _ADV_TYPE_FLAGS = const(0x01)
  19. _ADV_TYPE_NAME = const(0x09)
  20. _ADV_TYPE_UUID16_COMPLETE = const(0x3)
  21. _ADV_TYPE_UUID32_COMPLETE = const(0x5)
  22. _ADV_TYPE_UUID128_COMPLETE = const(0x7)
  23. _ADV_TYPE_UUID16_MORE = const(0x2)
  24. _ADV_TYPE_UUID32_MORE = const(0x4)
  25. _ADV_TYPE_UUID128_MORE = const(0x6)
  26. _ADV_TYPE_APPEARANCE = const(0x19)
  27. _ADV_TYPE_MANUFACTURER = const(0xFF)
  28. _ADV_PAYLOAD_MAX_LEN = const(31)
  29. _incoming_connection = None
  30. _connect_event = None
  31. def _peripheral_irq(event, data):
  32. global _incoming_connection
  33. if event == _IRQ_CENTRAL_CONNECT:
  34. conn_handle, addr_type, addr = data
  35. # Create, initialise, and register the device.
  36. device = Device(addr_type, bytes(addr))
  37. _incoming_connection = DeviceConnection(device)
  38. _incoming_connection._conn_handle = conn_handle
  39. DeviceConnection._connected[conn_handle] = _incoming_connection
  40. # Signal advertise() to return the connected device.
  41. _connect_event.set()
  42. elif event == _IRQ_CENTRAL_DISCONNECT:
  43. conn_handle, _, _ = data
  44. if connection := DeviceConnection._connected.get(conn_handle, None):
  45. # Tell the device_task that it should terminate.
  46. connection._event.set()
  47. def _peripheral_shutdown():
  48. global _incoming_connection, _connect_event
  49. _incoming_connection = None
  50. _connect_event = None
  51. register_irq_handler(_peripheral_irq, _peripheral_shutdown)
  52. # Advertising payloads are repeated packets of the following form:
  53. # 1 byte data length (N + 1)
  54. # 1 byte type (see constants below)
  55. # N bytes type-specific data
  56. def _append(adv_data, resp_data, adv_type, value):
  57. data = struct.pack("BB", len(value) + 1, adv_type) + value
  58. if len(data) + len(adv_data) < _ADV_PAYLOAD_MAX_LEN:
  59. adv_data += data
  60. return resp_data
  61. if len(data) + (len(resp_data) if resp_data else 0) < _ADV_PAYLOAD_MAX_LEN:
  62. if not resp_data:
  63. # Overflow into resp_data for the first time.
  64. resp_data = bytearray()
  65. resp_data += data
  66. return resp_data
  67. raise ValueError("Advertising payload too long")
  68. async def advertise(
  69. interval_us,
  70. adv_data=None,
  71. resp_data=None,
  72. connectable=True,
  73. limited_disc=False,
  74. br_edr=False,
  75. name=None,
  76. services=None,
  77. appearance=0,
  78. manufacturer=None,
  79. timeout_ms=None,
  80. ):
  81. global _incoming_connection, _connect_event
  82. ensure_active()
  83. if not adv_data and not resp_data:
  84. # If the user didn't manually specify adv_data / resp_data then
  85. # construct them from the kwargs. Keep adding fields to adv_data,
  86. # overflowing to resp_data if necessary.
  87. # TODO: Try and do better bin-packing than just concatenating in
  88. # order?
  89. adv_data = bytearray()
  90. resp_data = _append(
  91. adv_data,
  92. resp_data,
  93. _ADV_TYPE_FLAGS,
  94. struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)),
  95. )
  96. # Services are prioritised to go in the advertising data because iOS supports
  97. # filtering scan results by service only, so services must come first.
  98. if services:
  99. for uuid_len, code in (
  100. (2, _ADV_TYPE_UUID16_COMPLETE),
  101. (4, _ADV_TYPE_UUID32_COMPLETE),
  102. (16, _ADV_TYPE_UUID128_COMPLETE),
  103. ):
  104. if uuids := [bytes(uuid) for uuid in services if len(bytes(uuid)) == uuid_len]:
  105. resp_data = _append(adv_data, resp_data, code, b"".join(uuids))
  106. if name:
  107. resp_data = _append(adv_data, resp_data, _ADV_TYPE_NAME, name)
  108. if appearance:
  109. # See org.bluetooth.characteristic.gap.appearance.xml
  110. resp_data = _append(
  111. adv_data, resp_data, _ADV_TYPE_APPEARANCE, struct.pack("<H", appearance)
  112. )
  113. if manufacturer:
  114. resp_data = _append(
  115. adv_data,
  116. resp_data,
  117. _ADV_TYPE_MANUFACTURER,
  118. struct.pack("<H", manufacturer[0]) + manufacturer[1],
  119. )
  120. _connect_event = _connect_event or asyncio.ThreadSafeFlag()
  121. ble.gap_advertise(interval_us, adv_data=adv_data, resp_data=resp_data, connectable=connectable)
  122. try:
  123. # Allow optional timeout for a central to connect to us (or just to stop advertising).
  124. with DeviceTimeout(None, timeout_ms):
  125. await _connect_event.wait()
  126. # Get the newly connected connection to the central and start a task
  127. # to wait for disconnection.
  128. result = _incoming_connection
  129. _incoming_connection = None
  130. # This mirrors what connecting to a central does.
  131. result._run_task()
  132. return result
  133. except asyncio.CancelledError:
  134. # Something else cancelled this task (to manually stop advertising).
  135. ble.gap_advertise(None)
  136. except asyncio.TimeoutError:
  137. # DeviceTimeout waiting for connection.
  138. ble.gap_advertise(None)
  139. raise
  140. __version__ = '0.2.1'