diff options
Diffstat (limited to 'serial/urlhandler/protocol_socket.py')
-rw-r--r-- | serial/urlhandler/protocol_socket.py | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/serial/urlhandler/protocol_socket.py b/serial/urlhandler/protocol_socket.py new file mode 100644 index 0000000..2888467 --- /dev/null +++ b/serial/urlhandler/protocol_socket.py @@ -0,0 +1,359 @@ +#! python +# +# This module implements a simple socket based client. +# It does not support changing any port parameters and will silently ignore any +# requests to do so. +# +# The purpose of this module is that applications using pySerial can connect to +# TCP/IP to serial port converters that do not support RFC 2217. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2015 Chris Liechti <cliechti@gmx.net> +# +# SPDX-License-Identifier: BSD-3-Clause +# +# URL format: socket://<host>:<port>[/option[/option...]] +# options: +# - "debug" print diagnostic messages + +from __future__ import absolute_import + +import errno +import logging +import select +import socket +import time +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + +from serial.serialutil import SerialBase, SerialException, to_bytes, \ + PortNotOpenError, SerialTimeoutException, Timeout + +# map log level names to constants. used in from_url() +LOGGER_LEVELS = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, +} + +POLL_TIMEOUT = 5 + + +class Serial(SerialBase): + """Serial port implementation for plain sockets.""" + + BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200) + + def open(self): + """\ + Open port with current settings. This may throw a SerialException + if the port cannot be opened. + """ + self.logger = None + if self._port is None: + raise SerialException("Port must be configured before it can be used.") + if self.is_open: + raise SerialException("Port is already open.") + try: + # timeout is used for write timeout support :/ and to get an initial connection timeout + self._socket = socket.create_connection(self.from_url(self.portstr), timeout=POLL_TIMEOUT) + except Exception as msg: + self._socket = None + raise SerialException("Could not open port {}: {}".format(self.portstr, msg)) + # after connecting, switch to non-blocking, we're using select + self._socket.setblocking(False) + + # not that there is anything to configure... + self._reconfigure_port() + # all things set up get, now a clean start + self.is_open = True + if not self._dsrdtr: + self._update_dtr_state() + if not self._rtscts: + self._update_rts_state() + self.reset_input_buffer() + self.reset_output_buffer() + + def _reconfigure_port(self): + """\ + Set communication parameters on opened port. For the socket:// + protocol all settings are ignored! + """ + if self._socket is None: + raise SerialException("Can only operate on open ports") + if self.logger: + self.logger.info('ignored port configuration change') + + def close(self): + """Close port""" + if self.is_open: + if self._socket: + try: + self._socket.shutdown(socket.SHUT_RDWR) + self._socket.close() + except: + # ignore errors. + pass + self._socket = None + self.is_open = False + # in case of quick reconnects, give the server some time + time.sleep(0.3) + + def from_url(self, url): + """extract host and port from an URL string""" + parts = urlparse.urlsplit(url) + if parts.scheme != "socket": + raise SerialException( + 'expected a string in the form ' + '"socket://<host>:<port>[?logging={debug|info|warning|error}]": ' + 'not starting with socket:// ({!r})'.format(parts.scheme)) + try: + # process options now, directly altering self + for option, values in urlparse.parse_qs(parts.query, True).items(): + if option == 'logging': + logging.basicConfig() # XXX is that good to call it here? + self.logger = logging.getLogger('pySerial.socket') + self.logger.setLevel(LOGGER_LEVELS[values[0]]) + self.logger.debug('enabled logging') + else: + raise ValueError('unknown option: {!r}'.format(option)) + if not 0 <= parts.port < 65536: + raise ValueError("port not in range 0...65535") + except ValueError as e: + raise SerialException( + 'expected a string in the form ' + '"socket://<host>:<port>[?logging={debug|info|warning|error}]": {}'.format(e)) + + return (parts.hostname, parts.port) + + # - - - - - - - - - - - - - - - - - - - - - - - - + + @property + def in_waiting(self): + """Return the number of bytes currently in the input buffer.""" + if not self.is_open: + raise PortNotOpenError() + # Poll the socket to see if it is ready for reading. + # If ready, at least one byte will be to read. + lr, lw, lx = select.select([self._socket], [], [], 0) + return len(lr) + + # select based implementation, similar to posix, but only using socket API + # to be portable, additionally handle socket timeout which is used to + # emulate write timeouts + def read(self, size=1): + """\ + Read size bytes from the serial port. If a timeout is set it may + return less characters as requested. With no timeout it will block + until the requested number of bytes is read. + """ + if not self.is_open: + raise PortNotOpenError() + read = bytearray() + timeout = Timeout(self._timeout) + while len(read) < size: + try: + ready, _, _ = select.select([self._socket], [], [], timeout.time_left()) + # If select was used with a timeout, and the timeout occurs, it + # returns with empty lists -> thus abort read operation. + # For timeout == 0 (non-blocking operation) also abort when + # there is nothing to read. + if not ready: + break # timeout + buf = self._socket.recv(size - len(read)) + # read should always return some data as select reported it was + # ready to read when we get to this point, unless it is EOF + if not buf: + raise SerialException('socket disconnected') + read.extend(buf) + except OSError as e: + # this is for Python 3.x where select.error is a subclass of + # OSError ignore BlockingIOErrors and EINTR. other errors are shown + # https://www.python.org/dev/peps/pep-0475. + if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('read failed: {}'.format(e)) + except (select.error, socket.error) as e: + # this is for Python 2.x + # ignore BlockingIOErrors and EINTR. all errors are shown + # see also http://www.python.org/dev/peps/pep-3151/#select + if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('read failed: {}'.format(e)) + if timeout.expired(): + break + return bytes(read) + + def write(self, data): + """\ + Output the given byte string over the serial port. Can block if the + connection is blocked. May raise SerialException if the connection is + closed. + """ + if not self.is_open: + raise PortNotOpenError() + + d = to_bytes(data) + tx_len = length = len(d) + timeout = Timeout(self._write_timeout) + while tx_len > 0: + try: + n = self._socket.send(d) + if timeout.is_non_blocking: + # Zero timeout indicates non-blocking - simply return the + # number of bytes of data actually written + return n + elif not timeout.is_infinite: + # when timeout is set, use select to wait for being ready + # with the time left as timeout + if timeout.expired(): + raise SerialTimeoutException('Write timeout') + _, ready, _ = select.select([], [self._socket], [], timeout.time_left()) + if not ready: + raise SerialTimeoutException('Write timeout') + else: + assert timeout.time_left() is None + # wait for write operation + _, ready, _ = select.select([], [self._socket], [], None) + if not ready: + raise SerialException('write failed (select)') + d = d[n:] + tx_len -= n + except SerialException: + raise + except OSError as e: + # this is for Python 3.x where select.error is a subclass of + # OSError ignore BlockingIOErrors and EINTR. other errors are shown + # https://www.python.org/dev/peps/pep-0475. + if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('write failed: {}'.format(e)) + except select.error as e: + # this is for Python 2.x + # ignore BlockingIOErrors and EINTR. all errors are shown + # see also http://www.python.org/dev/peps/pep-3151/#select + if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('write failed: {}'.format(e)) + if not timeout.is_non_blocking and timeout.expired(): + raise SerialTimeoutException('Write timeout') + return length - len(d) + + def reset_input_buffer(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self.is_open: + raise PortNotOpenError() + + # just use recv to remove input, while there is some + ready = True + while ready: + ready, _, _ = select.select([self._socket], [], [], 0) + try: + if ready: + ready = self._socket.recv(4096) + except OSError as e: + # this is for Python 3.x where select.error is a subclass of + # OSError ignore BlockingIOErrors and EINTR. other errors are shown + # https://www.python.org/dev/peps/pep-0475. + if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('read failed: {}'.format(e)) + except (select.error, socket.error) as e: + # this is for Python 2.x + # ignore BlockingIOErrors and EINTR. all errors are shown + # see also http://www.python.org/dev/peps/pep-3151/#select + if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR): + raise SerialException('read failed: {}'.format(e)) + + def reset_output_buffer(self): + """\ + Clear output buffer, aborting the current output and + discarding all that is in the buffer. + """ + if not self.is_open: + raise PortNotOpenError() + if self.logger: + self.logger.info('ignored reset_output_buffer') + + def send_break(self, duration=0.25): + """\ + Send break condition. Timed, returns to idle state after given + duration. + """ + if not self.is_open: + raise PortNotOpenError() + if self.logger: + self.logger.info('ignored send_break({!r})'.format(duration)) + + def _update_break_state(self): + """Set break: Controls TXD. When active, to transmitting is + possible.""" + if self.logger: + self.logger.info('ignored _update_break_state({!r})'.format(self._break_state)) + + def _update_rts_state(self): + """Set terminal status line: Request To Send""" + if self.logger: + self.logger.info('ignored _update_rts_state({!r})'.format(self._rts_state)) + + def _update_dtr_state(self): + """Set terminal status line: Data Terminal Ready""" + if self.logger: + self.logger.info('ignored _update_dtr_state({!r})'.format(self._dtr_state)) + + @property + def cts(self): + """Read terminal status line: Clear To Send""" + if not self.is_open: + raise PortNotOpenError() + if self.logger: + self.logger.info('returning dummy for cts') + return True + + @property + def dsr(self): + """Read terminal status line: Data Set Ready""" + if not self.is_open: + raise PortNotOpenError() + if self.logger: + self.logger.info('returning dummy for dsr') + return True + + @property + def ri(self): + """Read terminal status line: Ring Indicator""" + if not self.is_open: + raise PortNotOpenError() + if self.logger: + self.logger.info('returning dummy for ri') + return False + + @property + def cd(self): + """Read terminal status line: Carrier Detect""" + if not self.is_open: + raise PortNotOpenError() + if self.logger: + self.logger.info('returning dummy for cd)') + return True + + # - - - platform specific - - - + + # works on Linux and probably all the other POSIX systems + def fileno(self): + """Get the file handle of the underlying socket for use with select""" + return self._socket.fileno() + + +# +# simple client test +if __name__ == '__main__': + import sys + s = Serial('socket://localhost:7000') + sys.stdout.write('{}\n'.format(s)) + + sys.stdout.write("write...\n") + s.write(b"hello\n") + s.flush() + sys.stdout.write("read: {}\n".format(s.read(5))) + + s.close() |