l2cap.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. # MicroPython aioble module
  2. # MIT license; Copyright (c) 2021 Jim Mussared
  3. from micropython import const
  4. import asyncio
  5. from .core import ble, log_error, register_irq_handler
  6. from .device import DeviceConnection
  7. _IRQ_L2CAP_ACCEPT = const(22)
  8. _IRQ_L2CAP_CONNECT = const(23)
  9. _IRQ_L2CAP_DISCONNECT = const(24)
  10. _IRQ_L2CAP_RECV = const(25)
  11. _IRQ_L2CAP_SEND_READY = const(26)
  12. # Once we start listening we're listening forever. (Limitation in NimBLE)
  13. _listening = False
  14. def _l2cap_irq(event, data):
  15. if event not in (
  16. _IRQ_L2CAP_CONNECT,
  17. _IRQ_L2CAP_DISCONNECT,
  18. _IRQ_L2CAP_RECV,
  19. _IRQ_L2CAP_SEND_READY,
  20. ):
  21. return
  22. # All the L2CAP events start with (conn_handle, cid, ...)
  23. if connection := DeviceConnection._connected.get(data[0], None):
  24. if channel := connection._l2cap_channel:
  25. # Expect to match the cid for this conn handle (unless we're
  26. # waiting for connection in which case channel._cid is None).
  27. if channel._cid is not None and channel._cid != data[1]:
  28. return
  29. # Update the channel object with new information.
  30. if event == _IRQ_L2CAP_CONNECT:
  31. _, channel._cid, _, channel.our_mtu, channel.peer_mtu = data
  32. elif event == _IRQ_L2CAP_DISCONNECT:
  33. _, _, psm, status = data
  34. channel._status = status
  35. channel._cid = None
  36. connection._l2cap_channel = None
  37. elif event == _IRQ_L2CAP_RECV:
  38. channel._data_ready = True
  39. elif event == _IRQ_L2CAP_SEND_READY:
  40. channel._stalled = False
  41. # Notify channel.
  42. channel._event.set()
  43. def _l2cap_shutdown():
  44. global _listening
  45. _listening = False
  46. register_irq_handler(_l2cap_irq, _l2cap_shutdown)
  47. # The channel was disconnected during a send/recvinto/flush.
  48. class L2CAPDisconnectedError(Exception):
  49. pass
  50. # Failed to connect to connection (argument is status).
  51. class L2CAPConnectionError(Exception):
  52. pass
  53. class L2CAPChannel:
  54. def __init__(self, connection):
  55. if not connection.is_connected():
  56. raise ValueError("Not connected")
  57. if connection._l2cap_channel:
  58. raise ValueError("Already has channel")
  59. connection._l2cap_channel = self
  60. self._connection = connection
  61. # Maximum size that the other side can send to us.
  62. self.our_mtu = 0
  63. # Maximum size that we can send.
  64. self.peer_mtu = 0
  65. # Set back to None on disconnection.
  66. self._cid = None
  67. # Set during disconnection.
  68. self._status = 0
  69. # If true, must wait for _IRQ_L2CAP_SEND_READY IRQ before sending.
  70. self._stalled = False
  71. # Has received a _IRQ_L2CAP_RECV since the buffer was last emptied.
  72. self._data_ready = False
  73. self._event = asyncio.ThreadSafeFlag()
  74. def _assert_connected(self):
  75. if self._cid is None:
  76. raise L2CAPDisconnectedError
  77. async def recvinto(self, buf, timeout_ms=None):
  78. self._assert_connected()
  79. # Wait until the data_ready flag is set. This flag is only ever set by
  80. # the event and cleared by this function.
  81. with self._connection.timeout(timeout_ms):
  82. while not self._data_ready:
  83. await self._event.wait()
  84. self._assert_connected()
  85. self._assert_connected()
  86. # Extract up to len(buf) bytes from the channel buffer.
  87. n = ble.l2cap_recvinto(self._connection._conn_handle, self._cid, buf)
  88. # Check if there's still remaining data in the channel buffers.
  89. self._data_ready = ble.l2cap_recvinto(self._connection._conn_handle, self._cid, None) > 0
  90. return n
  91. # Synchronously see if there's data ready.
  92. def available(self):
  93. self._assert_connected()
  94. return self._data_ready
  95. # Waits until the channel is free and then sends buf.
  96. # If the buffer is larger than the MTU it will be sent in chunks.
  97. async def send(self, buf, timeout_ms=None, chunk_size=None):
  98. offset = 0
  99. chunk_size = min(self.our_mtu * 2, self.peer_mtu, chunk_size or self.peer_mtu)
  100. mv = memoryview(buf)
  101. while offset < len(buf):
  102. if self._stalled:
  103. await self.flush(timeout_ms)
  104. # l2cap_send returns True if you can send immediately.
  105. self._assert_connected()
  106. self._stalled = not ble.l2cap_send(
  107. self._connection._conn_handle,
  108. self._cid,
  109. mv[offset : offset + chunk_size],
  110. )
  111. offset += chunk_size
  112. async def flush(self, timeout_ms=None):
  113. self._assert_connected()
  114. # Wait for the _stalled flag to be cleared by the IRQ.
  115. with self._connection.timeout(timeout_ms):
  116. while self._stalled:
  117. await self._event.wait()
  118. self._assert_connected()
  119. async def disconnect(self, timeout_ms=1000):
  120. if self._cid is None:
  121. return
  122. # Wait for the cid to be cleared by the disconnect IRQ.
  123. ble.l2cap_disconnect(self._connection._conn_handle, self._cid)
  124. await self.disconnected(timeout_ms)
  125. async def disconnected(self, timeout_ms=1000):
  126. with self._connection.timeout(timeout_ms):
  127. while self._cid is not None:
  128. await self._event.wait()
  129. # Context manager -- automatically disconnect.
  130. async def __aenter__(self):
  131. return self
  132. async def __aexit__(self, exc_type, exc_val, exc_traceback):
  133. await self.disconnect()
  134. # Use connection.l2cap_accept() instead of calling this directly.
  135. async def accept(connection, psm, mtu, timeout_ms):
  136. global _listening
  137. channel = L2CAPChannel(connection)
  138. # Start the stack listening if necessary.
  139. if not _listening:
  140. ble.l2cap_listen(psm, mtu)
  141. _listening = True
  142. # Wait for the connect irq from the remote connection.
  143. with connection.timeout(timeout_ms):
  144. await channel._event.wait()
  145. return channel
  146. # Use connection.l2cap_connect() instead of calling this directly.
  147. async def connect(connection, psm, mtu, timeout_ms):
  148. if _listening:
  149. raise ValueError("Can't connect while listening")
  150. channel = L2CAPChannel(connection)
  151. with connection.timeout(timeout_ms):
  152. ble.l2cap_connect(connection._conn_handle, psm, mtu)
  153. # Wait for the connect irq from the remote connection.
  154. # If the connection fails, we get a disconnect event (with status) instead.
  155. await channel._event.wait()
  156. if channel._cid is not None:
  157. return channel
  158. else:
  159. raise L2CAPConnectionError(channel._status)
  160. __version__ = '0.2.1'