__init__.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. #!/usr/bin/env python3
  2. #
  3. # Working with threading and pySerial
  4. #
  5. # This file is part of pySerial. https://github.com/pyserial/pyserial
  6. # (C) 2015-2016 Chris Liechti <cliechti@gmx.net>
  7. #
  8. # SPDX-License-Identifier: BSD-3-Clause
  9. """\
  10. Support threading with serial ports.
  11. """
  12. import serial
  13. import threading
  14. class Protocol(object):
  15. """\
  16. Protocol as used by the ReaderThread. This base class provides empty
  17. implementations of all methods.
  18. """
  19. def connection_made(self, transport):
  20. """Called when reader thread is started"""
  21. def data_received(self, data):
  22. """Called with snippets received from the serial port"""
  23. def connection_lost(self, exc):
  24. """\
  25. Called when the serial port is closed or the reader loop terminated
  26. otherwise.
  27. """
  28. if isinstance(exc, Exception):
  29. raise exc
  30. class Packetizer(Protocol):
  31. """
  32. Read binary packets from serial port. Packets are expected to be terminated
  33. with a TERMINATOR byte (null byte by default).
  34. The class also keeps track of the transport.
  35. """
  36. TERMINATOR = b'\0'
  37. def __init__(self):
  38. self.buffer = bytearray()
  39. self.transport = None
  40. def connection_made(self, transport):
  41. """Store transport"""
  42. self.transport = transport
  43. def connection_lost(self, exc):
  44. """Forget transport"""
  45. self.transport = None
  46. super(Packetizer, self).connection_lost(exc)
  47. def data_received(self, data):
  48. """Buffer received data, find TERMINATOR, call handle_packet"""
  49. self.buffer.extend(data)
  50. while self.TERMINATOR in self.buffer:
  51. packet, self.buffer = self.buffer.split(self.TERMINATOR, 1)
  52. self.handle_packet(packet)
  53. def handle_packet(self, packet):
  54. """Process packets - to be overridden by subclassing"""
  55. raise NotImplementedError('please implement functionality in handle_packet')
  56. class FramedPacket(Protocol):
  57. """
  58. Read binary packets. Packets are expected to have a start and stop marker.
  59. The class also keeps track of the transport.
  60. """
  61. START = b'('
  62. STOP = b')'
  63. def __init__(self):
  64. self.packet = bytearray()
  65. self.in_packet = False
  66. self.transport = None
  67. def connection_made(self, transport):
  68. """Store transport"""
  69. self.transport = transport
  70. def connection_lost(self, exc):
  71. """Forget transport"""
  72. self.transport = None
  73. self.in_packet = False
  74. del self.packet[:]
  75. super(FramedPacket, self).connection_lost(exc)
  76. def data_received(self, data):
  77. """Find data enclosed in START/STOP, call handle_packet"""
  78. for byte in serial.iterbytes(data):
  79. if byte == self.START:
  80. self.in_packet = True
  81. elif byte == self.STOP:
  82. self.in_packet = False
  83. self.handle_packet(bytes(self.packet)) # make read-only copy
  84. del self.packet[:]
  85. elif self.in_packet:
  86. self.packet.extend(byte)
  87. else:
  88. self.handle_out_of_packet_data(byte)
  89. def handle_packet(self, packet):
  90. """Process packets - to be overridden by subclassing"""
  91. raise NotImplementedError('please implement functionality in handle_packet')
  92. def handle_out_of_packet_data(self, data):
  93. """Process data that is received outside of packets"""
  94. pass
  95. class LineReader(Packetizer):
  96. """
  97. Read and write (Unicode) lines from/to serial port.
  98. The encoding is applied.
  99. """
  100. TERMINATOR = b'\r\n'
  101. ENCODING = 'utf-8'
  102. UNICODE_HANDLING = 'replace'
  103. def handle_packet(self, packet):
  104. self.handle_line(packet.decode(self.ENCODING, self.UNICODE_HANDLING))
  105. def handle_line(self, line):
  106. """Process one line - to be overridden by subclassing"""
  107. raise NotImplementedError('please implement functionality in handle_line')
  108. def write_line(self, text):
  109. """
  110. Write text to the transport. ``text`` is a Unicode string and the encoding
  111. is applied before sending ans also the newline is append.
  112. """
  113. # + is not the best choice but bytes does not support % or .format in py3 and we want a single write call
  114. self.transport.write(text.encode(self.ENCODING, self.UNICODE_HANDLING) + self.TERMINATOR)
  115. class ReaderThread(threading.Thread):
  116. """\
  117. Implement a serial port read loop and dispatch to a Protocol instance (like
  118. the asyncio.Protocol) but do it with threads.
  119. Calls to close() will close the serial port but it is also possible to just
  120. stop() this thread and continue the serial port instance otherwise.
  121. """
  122. def __init__(self, serial_instance, protocol_factory):
  123. """\
  124. Initialize thread.
  125. Note that the serial_instance' timeout is set to one second!
  126. Other settings are not changed.
  127. """
  128. super(ReaderThread, self).__init__()
  129. self.daemon = True
  130. self.serial = serial_instance
  131. self.protocol_factory = protocol_factory
  132. self.alive = True
  133. self._lock = threading.Lock()
  134. self._connection_made = threading.Event()
  135. self.protocol = None
  136. def stop(self):
  137. """Stop the reader thread"""
  138. self.alive = False
  139. if hasattr(self.serial, 'cancel_read'):
  140. self.serial.cancel_read()
  141. self.join(2)
  142. def run(self):
  143. """Reader loop"""
  144. if not hasattr(self.serial, 'cancel_read'):
  145. self.serial.timeout = 1
  146. self.protocol = self.protocol_factory()
  147. try:
  148. self.protocol.connection_made(self)
  149. except Exception as e:
  150. self.alive = False
  151. self.protocol.connection_lost(e)
  152. self._connection_made.set()
  153. return
  154. error = None
  155. self._connection_made.set()
  156. while self.alive and self.serial.is_open:
  157. try:
  158. # read all that is there or wait for one byte (blocking)
  159. data = self.serial.read(self.serial.in_waiting or 1)
  160. except serial.SerialException as e:
  161. # probably some I/O problem such as disconnected USB serial
  162. # adapters -> exit
  163. error = e
  164. break
  165. else:
  166. if data:
  167. # make a separated try-except for called used code
  168. try:
  169. self.protocol.data_received(data)
  170. except Exception as e:
  171. error = e
  172. break
  173. self.alive = False
  174. self.protocol.connection_lost(error)
  175. self.protocol = None
  176. def write(self, data):
  177. """Thread safe writing (uses lock)"""
  178. with self._lock:
  179. self.serial.write(data)
  180. def close(self):
  181. """Close the serial port and exit reader thread (uses lock)"""
  182. # use the lock to let other threads finish writing
  183. with self._lock:
  184. # first stop reading, so that closing can be done on idle port
  185. self.stop()
  186. self.serial.close()
  187. def connect(self):
  188. """
  189. Wait until connection is set up and return the transport and protocol
  190. instances.
  191. """
  192. if self.alive:
  193. self._connection_made.wait()
  194. if not self.alive:
  195. raise RuntimeError('connection_lost already called')
  196. return (self, self.protocol)
  197. else:
  198. raise RuntimeError('already stopped')
  199. # - - context manager, returns protocol
  200. def __enter__(self):
  201. """\
  202. Enter context handler. May raise RuntimeError in case the connection
  203. could not be created.
  204. """
  205. self.start()
  206. self._connection_made.wait()
  207. if not self.alive:
  208. raise RuntimeError('connection_lost already called')
  209. return self.protocol
  210. def __exit__(self, exc_type, exc_val, exc_tb):
  211. """Leave context: close port"""
  212. self.close()
  213. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  214. # test
  215. if __name__ == '__main__':
  216. # pylint: disable=wrong-import-position
  217. import sys
  218. import time
  219. import traceback
  220. #~ PORT = 'spy:///dev/ttyUSB0'
  221. PORT = 'loop://'
  222. class PrintLines(LineReader):
  223. def connection_made(self, transport):
  224. super(PrintLines, self).connection_made(transport)
  225. sys.stdout.write('port opened\n')
  226. self.write_line('hello world')
  227. def handle_line(self, data):
  228. sys.stdout.write('line received: {!r}\n'.format(data))
  229. def connection_lost(self, exc):
  230. if exc:
  231. traceback.print_exc(exc)
  232. sys.stdout.write('port closed\n')
  233. ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
  234. with ReaderThread(ser, PrintLines) as protocol:
  235. protocol.write_line('hello')
  236. time.sleep(2)
  237. # alternative usage
  238. ser = serial.serial_for_url(PORT, baudrate=115200, timeout=1)
  239. t = ReaderThread(ser, PrintLines)
  240. t.start()
  241. transport, protocol = t.connect()
  242. protocol.write_line('hello')
  243. time.sleep(2)
  244. t.close()