protocol_socket.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. #! python
  2. #
  3. # This module implements a simple socket based client.
  4. # It does not support changing any port parameters and will silently ignore any
  5. # requests to do so.
  6. #
  7. # The purpose of this module is that applications using pySerial can connect to
  8. # TCP/IP to serial port converters that do not support RFC 2217.
  9. #
  10. # This file is part of pySerial. https://github.com/pyserial/pyserial
  11. # (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
  12. #
  13. # SPDX-License-Identifier: BSD-3-Clause
  14. #
  15. # URL format: socket://<host>:<port>[/option[/option...]]
  16. # options:
  17. # - "debug" print diagnostic messages
  18. import errno
  19. import logging
  20. import select
  21. import socket
  22. import time
  23. try:
  24. import urlparse
  25. except ImportError:
  26. import urllib.parse as urlparse
  27. from serial.serialutil import SerialBase, SerialException, portNotOpenError, to_bytes
  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. POLL_TIMEOUT = 5
  36. class Serial(SerialBase):
  37. """Serial port implementation for plain sockets."""
  38. BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
  39. 9600, 19200, 38400, 57600, 115200)
  40. def open(self):
  41. """\
  42. Open port with current settings. This may throw a SerialException
  43. if the port cannot be opened.
  44. """
  45. self.logger = None
  46. if self._port is None:
  47. raise SerialException("Port must be configured before it can be used.")
  48. if self.is_open:
  49. raise SerialException("Port is already open.")
  50. try:
  51. # timeout is used for write timeout support :/ and to get an initial connection timeout
  52. self._socket = socket.create_connection(self.from_url(self.portstr), timeout=POLL_TIMEOUT)
  53. except Exception as msg:
  54. self._socket = None
  55. raise SerialException("Could not open port {}: {}".format(self.portstr, msg))
  56. # not that there is anything to configure...
  57. self._reconfigure_port()
  58. # all things set up get, now a clean start
  59. self.is_open = True
  60. if not self._dsrdtr:
  61. self._update_dtr_state()
  62. if not self._rtscts:
  63. self._update_rts_state()
  64. self.reset_input_buffer()
  65. self.reset_output_buffer()
  66. def _reconfigure_port(self):
  67. """\
  68. Set communication parameters on opened port. For the socket://
  69. protocol all settings are ignored!
  70. """
  71. if self._socket is None:
  72. raise SerialException("Can only operate on open ports")
  73. if self.logger:
  74. self.logger.info('ignored port configuration change')
  75. def close(self):
  76. """Close port"""
  77. if self.is_open:
  78. if self._socket:
  79. try:
  80. self._socket.shutdown(socket.SHUT_RDWR)
  81. self._socket.close()
  82. except:
  83. # ignore errors.
  84. pass
  85. self._socket = None
  86. self.is_open = False
  87. # in case of quick reconnects, give the server some time
  88. time.sleep(0.3)
  89. def from_url(self, url):
  90. """extract host and port from an URL string"""
  91. parts = urlparse.urlsplit(url)
  92. if parts.scheme != "socket":
  93. raise SerialException(
  94. 'expected a string in the form '
  95. '"socket://<host>:<port>[?logging={debug|info|warning|error}]": '
  96. 'not starting with socket:// ({!r})'.format(parts.scheme))
  97. try:
  98. # process options now, directly altering self
  99. for option, values in urlparse.parse_qs(parts.query, True).items():
  100. if option == 'logging':
  101. logging.basicConfig() # XXX is that good to call it here?
  102. self.logger = logging.getLogger('pySerial.socket')
  103. self.logger.setLevel(LOGGER_LEVELS[values[0]])
  104. self.logger.debug('enabled logging')
  105. else:
  106. raise ValueError('unknown option: {!r}'.format(option))
  107. if not 0 <= parts.port < 65536:
  108. raise ValueError("port not in range 0...65535")
  109. except ValueError as e:
  110. raise SerialException(
  111. 'expected a string in the form '
  112. '"socket://<host>:<port>[?logging={debug|info|warning|error}]": {}'.format(e))
  113. return (parts.hostname, parts.port)
  114. # - - - - - - - - - - - - - - - - - - - - - - - -
  115. @property
  116. def in_waiting(self):
  117. """Return the number of bytes currently in the input buffer."""
  118. if not self.is_open:
  119. raise portNotOpenError
  120. # Poll the socket to see if it is ready for reading.
  121. # If ready, at least one byte will be to read.
  122. lr, lw, lx = select.select([self._socket], [], [], 0)
  123. return len(lr)
  124. # select based implementation, similar to posix, but only using socket API
  125. # to be portable, additionally handle socket timeout which is used to
  126. # emulate write timeouts
  127. def read(self, size=1):
  128. """\
  129. Read size bytes from the serial port. If a timeout is set it may
  130. return less characters as requested. With no timeout it will block
  131. until the requested number of bytes is read.
  132. """
  133. if not self.is_open:
  134. raise portNotOpenError
  135. read = bytearray()
  136. timeout = self._timeout
  137. while len(read) < size:
  138. try:
  139. start_time = time.time()
  140. ready, _, _ = select.select([self._socket], [], [], timeout)
  141. # If select was used with a timeout, and the timeout occurs, it
  142. # returns with empty lists -> thus abort read operation.
  143. # For timeout == 0 (non-blocking operation) also abort when
  144. # there is nothing to read.
  145. if not ready:
  146. break # timeout
  147. buf = self._socket.recv(size - len(read))
  148. # read should always return some data as select reported it was
  149. # ready to read when we get to this point, unless it is EOF
  150. if not buf:
  151. raise SerialException('socket disconnected')
  152. read.extend(buf)
  153. if timeout is not None:
  154. timeout -= time.time() - start_time
  155. if timeout <= 0:
  156. break
  157. except socket.timeout:
  158. # timeout is used for write support, just go reading again
  159. pass
  160. except socket.error as e:
  161. # connection fails -> terminate loop
  162. raise SerialException('connection failed ({})'.format(e))
  163. except OSError as e:
  164. # this is for Python 3.x where select.error is a subclass of
  165. # OSError ignore EAGAIN errors. all other errors are shown
  166. if e.errno != errno.EAGAIN:
  167. raise SerialException('read failed: {}'.format(e))
  168. except select.error as e:
  169. # this is for Python 2.x
  170. # ignore EAGAIN errors. all other errors are shown
  171. # see also http://www.python.org/dev/peps/pep-3151/#select
  172. if e[0] != errno.EAGAIN:
  173. raise SerialException('read failed: {}'.format(e))
  174. return bytes(read)
  175. def write(self, data):
  176. """\
  177. Output the given byte string over the serial port. Can block if the
  178. connection is blocked. May raise SerialException if the connection is
  179. closed.
  180. """
  181. if not self.is_open:
  182. raise portNotOpenError
  183. try:
  184. self._socket.sendall(to_bytes(data))
  185. except socket.error as e:
  186. # XXX what exception if socket connection fails
  187. raise SerialException("socket connection failed: {}".format(e))
  188. return len(data)
  189. def reset_input_buffer(self):
  190. """Clear input buffer, discarding all that is in the buffer."""
  191. if not self.is_open:
  192. raise portNotOpenError
  193. if self.logger:
  194. self.logger.info('ignored reset_input_buffer')
  195. def reset_output_buffer(self):
  196. """\
  197. Clear output buffer, aborting the current output and
  198. discarding all that is in the buffer.
  199. """
  200. if not self.is_open:
  201. raise portNotOpenError
  202. if self.logger:
  203. self.logger.info('ignored reset_output_buffer')
  204. def send_break(self, duration=0.25):
  205. """\
  206. Send break condition. Timed, returns to idle state after given
  207. duration.
  208. """
  209. if not self.is_open:
  210. raise portNotOpenError
  211. if self.logger:
  212. self.logger.info('ignored send_break({!r})'.format(duration))
  213. def _update_break_state(self):
  214. """Set break: Controls TXD. When active, to transmitting is
  215. possible."""
  216. if self.logger:
  217. self.logger.info('ignored _update_break_state({!r})'.format(self._break_state))
  218. def _update_rts_state(self):
  219. """Set terminal status line: Request To Send"""
  220. if self.logger:
  221. self.logger.info('ignored _update_rts_state({!r})'.format(self._rts_state))
  222. def _update_dtr_state(self):
  223. """Set terminal status line: Data Terminal Ready"""
  224. if self.logger:
  225. self.logger.info('ignored _update_dtr_state({!r})'.format(self._dtr_state))
  226. @property
  227. def cts(self):
  228. """Read terminal status line: Clear To Send"""
  229. if not self.is_open:
  230. raise portNotOpenError
  231. if self.logger:
  232. self.logger.info('returning dummy for cts')
  233. return True
  234. @property
  235. def dsr(self):
  236. """Read terminal status line: Data Set Ready"""
  237. if not self.is_open:
  238. raise portNotOpenError
  239. if self.logger:
  240. self.logger.info('returning dummy for dsr')
  241. return True
  242. @property
  243. def ri(self):
  244. """Read terminal status line: Ring Indicator"""
  245. if not self.is_open:
  246. raise portNotOpenError
  247. if self.logger:
  248. self.logger.info('returning dummy for ri')
  249. return False
  250. @property
  251. def cd(self):
  252. """Read terminal status line: Carrier Detect"""
  253. if not self.is_open:
  254. raise portNotOpenError
  255. if self.logger:
  256. self.logger.info('returning dummy for cd)')
  257. return True
  258. # - - - platform specific - - -
  259. # works on Linux and probably all the other POSIX systems
  260. def fileno(self):
  261. """Get the file handle of the underlying socket for use with select"""
  262. return self._socket.fileno()
  263. #
  264. # simple client test
  265. if __name__ == '__main__':
  266. import sys
  267. s = Serial('socket://localhost:7000')
  268. sys.stdout.write('{}\n'.format(s))
  269. sys.stdout.write("write...\n")
  270. s.write(b"hello\n")
  271. s.flush()
  272. sys.stdout.write("read: {}\n".format(s.read(5)))
  273. s.close()