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
19from __future__ import absolute_import
20
21import errno
22import logging
23import select
24import socket
25import time
26try:
27    import urlparse
28except ImportError:
29    import urllib.parse as urlparse
30
31from serial.serialutil import SerialBase, SerialException, to_bytes, \
32    PortNotOpenError, SerialTimeoutException, Timeout
33
34# map log level names to constants. used in from_url()
35LOGGER_LEVELS = {
36    'debug': logging.DEBUG,
37    'info': logging.INFO,
38    'warning': logging.WARNING,
39    'error': logging.ERROR,
40}
41
42POLL_TIMEOUT = 5
43
44
45class Serial(SerialBase):
46    """Serial port implementation for plain sockets."""
47
48    BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
49                 9600, 19200, 38400, 57600, 115200)
50
51    def open(self):
52        """\
53        Open port with current settings. This may throw a SerialException
54        if the port cannot be opened.
55        """
56        self.logger = None
57        if self._port is None:
58            raise SerialException("Port must be configured before it can be used.")
59        if self.is_open:
60            raise SerialException("Port is already open.")
61        try:
62            # timeout is used for write timeout support :/ and to get an initial connection timeout
63            self._socket = socket.create_connection(self.from_url(self.portstr), timeout=POLL_TIMEOUT)
64        except Exception as msg:
65            self._socket = None
66            raise SerialException("Could not open port {}: {}".format(self.portstr, msg))
67        # after connecting, switch to non-blocking, we're using select
68        self._socket.setblocking(False)
69
70        # not that there is anything to configure...
71        self._reconfigure_port()
72        # all things set up get, now a clean start
73        self.is_open = True
74        if not self._dsrdtr:
75            self._update_dtr_state()
76        if not self._rtscts:
77            self._update_rts_state()
78        self.reset_input_buffer()
79        self.reset_output_buffer()
80
81    def _reconfigure_port(self):
82        """\
83        Set communication parameters on opened port. For the socket://
84        protocol all settings are ignored!
85        """
86        if self._socket is None:
87            raise SerialException("Can only operate on open ports")
88        if self.logger:
89            self.logger.info('ignored port configuration change')
90
91    def close(self):
92        """Close port"""
93        if self.is_open:
94            if self._socket:
95                try:
96                    self._socket.shutdown(socket.SHUT_RDWR)
97                    self._socket.close()
98                except:
99                    # ignore errors.
100                    pass
101                self._socket = None
102            self.is_open = False
103            # in case of quick reconnects, give the server some time
104            time.sleep(0.3)
105
106    def from_url(self, url):
107        """extract host and port from an URL string"""
108        parts = urlparse.urlsplit(url)
109        if parts.scheme != "socket":
110            raise SerialException(
111                'expected a string in the form '
112                '"socket://<host>:<port>[?logging={debug|info|warning|error}]": '
113                'not starting with socket:// ({!r})'.format(parts.scheme))
114        try:
115            # process options now, directly altering self
116            for option, values in urlparse.parse_qs(parts.query, True).items():
117                if option == 'logging':
118                    logging.basicConfig()   # XXX is that good to call it here?
119                    self.logger = logging.getLogger('pySerial.socket')
120                    self.logger.setLevel(LOGGER_LEVELS[values[0]])
121                    self.logger.debug('enabled logging')
122                else:
123                    raise ValueError('unknown option: {!r}'.format(option))
124            if not 0 <= parts.port < 65536:
125                raise ValueError("port not in range 0...65535")
126        except ValueError as e:
127            raise SerialException(
128                'expected a string in the form '
129                '"socket://<host>:<port>[?logging={debug|info|warning|error}]": {}'.format(e))
130
131        return (parts.hostname, parts.port)
132
133    #  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
134
135    @property
136    def in_waiting(self):
137        """Return the number of bytes currently in the input buffer."""
138        if not self.is_open:
139            raise PortNotOpenError()
140        # Poll the socket to see if it is ready for reading.
141        # If ready, at least one byte will be to read.
142        lr, lw, lx = select.select([self._socket], [], [], 0)
143        return len(lr)
144
145    # select based implementation, similar to posix, but only using socket API
146    # to be portable, additionally handle socket timeout which is used to
147    # emulate write timeouts
148    def read(self, size=1):
149        """\
150        Read size bytes from the serial port. If a timeout is set it may
151        return less characters as requested. With no timeout it will block
152        until the requested number of bytes is read.
153        """
154        if not self.is_open:
155            raise PortNotOpenError()
156        read = bytearray()
157        timeout = Timeout(self._timeout)
158        while len(read) < size:
159            try:
160                ready, _, _ = select.select([self._socket], [], [], timeout.time_left())
161                # If select was used with a timeout, and the timeout occurs, it
162                # returns with empty lists -> thus abort read operation.
163                # For timeout == 0 (non-blocking operation) also abort when
164                # there is nothing to read.
165                if not ready:
166                    break   # timeout
167                buf = self._socket.recv(size - len(read))
168                # read should always return some data as select reported it was
169                # ready to read when we get to this point, unless it is EOF
170                if not buf:
171                    raise SerialException('socket disconnected')
172                read.extend(buf)
173            except OSError as e:
174                # this is for Python 3.x where select.error is a subclass of
175                # OSError ignore BlockingIOErrors and EINTR. other errors are shown
176                # https://www.python.org/dev/peps/pep-0475.
177                if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
178                    raise SerialException('read failed: {}'.format(e))
179            except (select.error, socket.error) as e:
180                # this is for Python 2.x
181                # ignore BlockingIOErrors and EINTR. all errors are shown
182                # see also http://www.python.org/dev/peps/pep-3151/#select
183                if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
184                    raise SerialException('read failed: {}'.format(e))
185            if timeout.expired():
186                break
187        return bytes(read)
188
189    def write(self, data):
190        """\
191        Output the given byte string over the serial port. Can block if the
192        connection is blocked. May raise SerialException if the connection is
193        closed.
194        """
195        if not self.is_open:
196            raise PortNotOpenError()
197
198        d = to_bytes(data)
199        tx_len = length = len(d)
200        timeout = Timeout(self._write_timeout)
201        while tx_len > 0:
202            try:
203                n = self._socket.send(d)
204                if timeout.is_non_blocking:
205                    # Zero timeout indicates non-blocking - simply return the
206                    # number of bytes of data actually written
207                    return n
208                elif not timeout.is_infinite:
209                    # when timeout is set, use select to wait for being ready
210                    # with the time left as timeout
211                    if timeout.expired():
212                        raise SerialTimeoutException('Write timeout')
213                    _, ready, _ = select.select([], [self._socket], [], timeout.time_left())
214                    if not ready:
215                        raise SerialTimeoutException('Write timeout')
216                else:
217                    assert timeout.time_left() is None
218                    # wait for write operation
219                    _, ready, _ = select.select([], [self._socket], [], None)
220                    if not ready:
221                        raise SerialException('write failed (select)')
222                d = d[n:]
223                tx_len -= n
224            except SerialException:
225                raise
226            except OSError as e:
227                # this is for Python 3.x where select.error is a subclass of
228                # OSError ignore BlockingIOErrors and EINTR. other errors are shown
229                # https://www.python.org/dev/peps/pep-0475.
230                if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
231                    raise SerialException('write failed: {}'.format(e))
232            except select.error as e:
233                # this is for Python 2.x
234                # ignore BlockingIOErrors and EINTR. all errors are shown
235                # see also http://www.python.org/dev/peps/pep-3151/#select
236                if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
237                    raise SerialException('write failed: {}'.format(e))
238            if not timeout.is_non_blocking and timeout.expired():
239                raise SerialTimeoutException('Write timeout')
240        return length - len(d)
241
242    def reset_input_buffer(self):
243        """Clear input buffer, discarding all that is in the buffer."""
244        if not self.is_open:
245            raise PortNotOpenError()
246
247        # just use recv to remove input, while there is some
248        ready = True
249        while ready:
250            ready, _, _ = select.select([self._socket], [], [], 0)
251            try:
252                if ready:
253                    ready = self._socket.recv(4096)
254            except OSError as e:
255                # this is for Python 3.x where select.error is a subclass of
256                # OSError ignore BlockingIOErrors and EINTR. other errors are shown
257                # https://www.python.org/dev/peps/pep-0475.
258                if e.errno not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
259                    raise SerialException('read failed: {}'.format(e))
260            except (select.error, socket.error) as e:
261                # this is for Python 2.x
262                # ignore BlockingIOErrors and EINTR. all errors are shown
263                # see also http://www.python.org/dev/peps/pep-3151/#select
264                if e[0] not in (errno.EAGAIN, errno.EALREADY, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EINTR):
265                    raise SerialException('read failed: {}'.format(e))
266
267    def reset_output_buffer(self):
268        """\
269        Clear output buffer, aborting the current output and
270        discarding all that is in the buffer.
271        """
272        if not self.is_open:
273            raise PortNotOpenError()
274        if self.logger:
275            self.logger.info('ignored reset_output_buffer')
276
277    def send_break(self, duration=0.25):
278        """\
279        Send break condition. Timed, returns to idle state after given
280        duration.
281        """
282        if not self.is_open:
283            raise PortNotOpenError()
284        if self.logger:
285            self.logger.info('ignored send_break({!r})'.format(duration))
286
287    def _update_break_state(self):
288        """Set break: Controls TXD. When active, to transmitting is
289        possible."""
290        if self.logger:
291            self.logger.info('ignored _update_break_state({!r})'.format(self._break_state))
292
293    def _update_rts_state(self):
294        """Set terminal status line: Request To Send"""
295        if self.logger:
296            self.logger.info('ignored _update_rts_state({!r})'.format(self._rts_state))
297
298    def _update_dtr_state(self):
299        """Set terminal status line: Data Terminal Ready"""
300        if self.logger:
301            self.logger.info('ignored _update_dtr_state({!r})'.format(self._dtr_state))
302
303    @property
304    def cts(self):
305        """Read terminal status line: Clear To Send"""
306        if not self.is_open:
307            raise PortNotOpenError()
308        if self.logger:
309            self.logger.info('returning dummy for cts')
310        return True
311
312    @property
313    def dsr(self):
314        """Read terminal status line: Data Set Ready"""
315        if not self.is_open:
316            raise PortNotOpenError()
317        if self.logger:
318            self.logger.info('returning dummy for dsr')
319        return True
320
321    @property
322    def ri(self):
323        """Read terminal status line: Ring Indicator"""
324        if not self.is_open:
325            raise PortNotOpenError()
326        if self.logger:
327            self.logger.info('returning dummy for ri')
328        return False
329
330    @property
331    def cd(self):
332        """Read terminal status line: Carrier Detect"""
333        if not self.is_open:
334            raise PortNotOpenError()
335        if self.logger:
336            self.logger.info('returning dummy for cd)')
337        return True
338
339    # - - - platform specific - - -
340
341    # works on Linux and probably all the other POSIX systems
342    def fileno(self):
343        """Get the file handle of the underlying socket for use with select"""
344        return self._socket.fileno()
345
346
347#
348# simple client test
349if __name__ == '__main__':
350    import sys
351    s = Serial('socket://localhost:7000')
352    sys.stdout.write('{}\n'.format(s))
353
354    sys.stdout.write("write...\n")
355    s.write(b"hello\n")
356    s.flush()
357    sys.stdout.write("read: {}\n".format(s.read(5)))
358
359    s.close()
360