protocol_loop.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. #! python
  2. #
  3. # This module implements a loop back connection receiving itself what it sent.
  4. #
  5. # The purpose of this module is.. well... You can run the unit tests with it.
  6. # and it was so easy to implement ;-)
  7. #
  8. # This file is part of pySerial. https://github.com/pyserial/pyserial
  9. # (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
  10. #
  11. # SPDX-License-Identifier: BSD-3-Clause
  12. #
  13. # URL format: loop://[option[/option...]]
  14. # options:
  15. # - "debug" print diagnostic messages
  16. import logging
  17. import numbers
  18. import time
  19. try:
  20. import urlparse
  21. except ImportError:
  22. import urllib.parse as urlparse
  23. try:
  24. import queue
  25. except ImportError:
  26. import Queue as queue
  27. from serial.serialutil import SerialBase, SerialException, to_bytes, iterbytes, writeTimeoutError, portNotOpenError
  28. # map log level names to constants. used in from_url()
  29. LOGGER_LEVELS = {
  30. 'debug': logging.DEBUG,
  31. 'info': logging.INFO,
  32. 'warning': logging.WARNING,
  33. 'error': logging.ERROR,
  34. }
  35. class Serial(SerialBase):
  36. """Serial port implementation that simulates a loop back connection in plain software."""
  37. BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
  38. 9600, 19200, 38400, 57600, 115200)
  39. def __init__(self, *args, **kwargs):
  40. self.buffer_size = 4096
  41. self.queue = None
  42. self.logger = None
  43. self._cancel_write = False
  44. super(Serial, self).__init__(*args, **kwargs)
  45. def open(self):
  46. """\
  47. Open port with current settings. This may throw a SerialException
  48. if the port cannot be opened.
  49. """
  50. if self.is_open:
  51. raise SerialException("Port is already open.")
  52. self.logger = None
  53. self.queue = queue.Queue(self.buffer_size)
  54. if self._port is None:
  55. raise SerialException("Port must be configured before it can be used.")
  56. # not that there is anything to open, but the function applies the
  57. # options found in the URL
  58. self.from_url(self.port)
  59. # not that there anything to configure...
  60. self._reconfigure_port()
  61. # all things set up get, now a clean start
  62. self.is_open = True
  63. if not self._dsrdtr:
  64. self._update_dtr_state()
  65. if not self._rtscts:
  66. self._update_rts_state()
  67. self.reset_input_buffer()
  68. self.reset_output_buffer()
  69. def close(self):
  70. if self.is_open:
  71. self.is_open = False
  72. try:
  73. self.queue.put_nowait(None)
  74. except queue.Full:
  75. pass
  76. super(Serial, self).close()
  77. def _reconfigure_port(self):
  78. """\
  79. Set communication parameters on opened port. For the loop://
  80. protocol all settings are ignored!
  81. """
  82. # not that's it of any real use, but it helps in the unit tests
  83. if not isinstance(self._baudrate, numbers.Integral) or not 0 < self._baudrate < 2 ** 32:
  84. raise ValueError("invalid baudrate: {!r}".format(self._baudrate))
  85. if self.logger:
  86. self.logger.info('_reconfigure_port()')
  87. def from_url(self, url):
  88. """extract host and port from an URL string"""
  89. parts = urlparse.urlsplit(url)
  90. if parts.scheme != "loop":
  91. raise SerialException(
  92. 'expected a string in the form '
  93. '"loop://[?logging={debug|info|warning|error}]": not starting '
  94. 'with loop:// ({!r})'.format(parts.scheme))
  95. try:
  96. # process options now, directly altering self
  97. for option, values in urlparse.parse_qs(parts.query, True).items():
  98. if option == 'logging':
  99. logging.basicConfig() # XXX is that good to call it here?
  100. self.logger = logging.getLogger('pySerial.loop')
  101. self.logger.setLevel(LOGGER_LEVELS[values[0]])
  102. self.logger.debug('enabled logging')
  103. else:
  104. raise ValueError('unknown option: {!r}'.format(option))
  105. except ValueError as e:
  106. raise SerialException(
  107. 'expected a string in the form '
  108. '"loop://[?logging={debug|info|warning|error}]": {}'.format(e))
  109. # - - - - - - - - - - - - - - - - - - - - - - - -
  110. @property
  111. def in_waiting(self):
  112. """Return the number of bytes currently in the input buffer."""
  113. if not self.is_open:
  114. raise portNotOpenError
  115. if self.logger:
  116. # attention the logged value can differ from return value in
  117. # threaded environments...
  118. self.logger.debug('in_waiting -> {:d}'.format(self.queue.qsize()))
  119. return self.queue.qsize()
  120. def read(self, size=1):
  121. """\
  122. Read size bytes from the serial port. If a timeout is set it may
  123. return less characters as requested. With no timeout it will block
  124. until the requested number of bytes is read.
  125. """
  126. if not self.is_open:
  127. raise portNotOpenError
  128. if self._timeout is not None and self._timeout != 0:
  129. timeout = time.time() + self._timeout
  130. else:
  131. timeout = None
  132. data = bytearray()
  133. while size > 0 and self.is_open:
  134. try:
  135. b = self.queue.get(timeout=self._timeout) # XXX inter char timeout
  136. except queue.Empty:
  137. if self._timeout == 0:
  138. break
  139. else:
  140. if b is not None:
  141. data += b
  142. size -= 1
  143. else:
  144. break
  145. # check for timeout now, after data has been read.
  146. # useful for timeout = 0 (non blocking) read
  147. if timeout and time.time() > timeout:
  148. if self.logger:
  149. self.logger.info('read timeout')
  150. break
  151. return bytes(data)
  152. def cancel_read(self):
  153. self.queue.put_nowait(None)
  154. def cancel_write(self):
  155. self._cancel_write = True
  156. def write(self, data):
  157. """\
  158. Output the given byte string over the serial port. Can block if the
  159. connection is blocked. May raise SerialException if the connection is
  160. closed.
  161. """
  162. self._cancel_write = False
  163. if not self.is_open:
  164. raise portNotOpenError
  165. data = to_bytes(data)
  166. # calculate aprox time that would be used to send the data
  167. time_used_to_send = 10.0 * len(data) / self._baudrate
  168. # when a write timeout is configured check if we would be successful
  169. # (not sending anything, not even the part that would have time)
  170. if self._write_timeout is not None and time_used_to_send > self._write_timeout:
  171. # must wait so that unit test succeeds
  172. time_left = self._write_timeout
  173. while time_left > 0 and not self._cancel_write:
  174. time.sleep(min(time_left, 0.5))
  175. time_left -= 0.5
  176. if self._cancel_write:
  177. return 0 # XXX
  178. raise writeTimeoutError
  179. for byte in iterbytes(data):
  180. self.queue.put(byte, timeout=self._write_timeout)
  181. return len(data)
  182. def reset_input_buffer(self):
  183. """Clear input buffer, discarding all that is in the buffer."""
  184. if not self.is_open:
  185. raise portNotOpenError
  186. if self.logger:
  187. self.logger.info('reset_input_buffer()')
  188. try:
  189. while self.queue.qsize():
  190. self.queue.get_nowait()
  191. except queue.Empty:
  192. pass
  193. def reset_output_buffer(self):
  194. """\
  195. Clear output buffer, aborting the current output and
  196. discarding all that is in the buffer.
  197. """
  198. if not self.is_open:
  199. raise portNotOpenError
  200. if self.logger:
  201. self.logger.info('reset_output_buffer()')
  202. try:
  203. while self.queue.qsize():
  204. self.queue.get_nowait()
  205. except queue.Empty:
  206. pass
  207. def _update_break_state(self):
  208. """\
  209. Set break: Controls TXD. When active, to transmitting is
  210. possible.
  211. """
  212. if self.logger:
  213. self.logger.info('_update_break_state({!r})'.format(self._break_state))
  214. def _update_rts_state(self):
  215. """Set terminal status line: Request To Send"""
  216. if self.logger:
  217. self.logger.info('_update_rts_state({!r}) -> state of CTS'.format(self._rts_state))
  218. def _update_dtr_state(self):
  219. """Set terminal status line: Data Terminal Ready"""
  220. if self.logger:
  221. self.logger.info('_update_dtr_state({!r}) -> state of DSR'.format(self._dtr_state))
  222. @property
  223. def cts(self):
  224. """Read terminal status line: Clear To Send"""
  225. if not self.is_open:
  226. raise portNotOpenError
  227. if self.logger:
  228. self.logger.info('CTS -> state of RTS ({!r})'.format(self._rts_state))
  229. return self._rts_state
  230. @property
  231. def dsr(self):
  232. """Read terminal status line: Data Set Ready"""
  233. if self.logger:
  234. self.logger.info('DSR -> state of DTR ({!r})'.format(self._dtr_state))
  235. return self._dtr_state
  236. @property
  237. def ri(self):
  238. """Read terminal status line: Ring Indicator"""
  239. if not self.is_open:
  240. raise portNotOpenError
  241. if self.logger:
  242. self.logger.info('returning dummy for RI')
  243. return False
  244. @property
  245. def cd(self):
  246. """Read terminal status line: Carrier Detect"""
  247. if not self.is_open:
  248. raise portNotOpenError
  249. if self.logger:
  250. self.logger.info('returning dummy for CD')
  251. return True
  252. # - - - platform specific - - -
  253. # None so far
  254. # simple client test
  255. if __name__ == '__main__':
  256. import sys
  257. s = Serial('loop://')
  258. sys.stdout.write('{}\n'.format(s))
  259. sys.stdout.write("write...\n")
  260. s.write("hello\n")
  261. s.flush()
  262. sys.stdout.write("read: {!r}\n".format(s.read(5)))
  263. s.close()