• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#! python
2#
3# Python Serial Port Extension for Win32, Linux, BSD, Jython
4# see __init__.py
5#
6# This module implements a simple socket based client.
7# It does not support changing any port parameters and will silently ignore any
8# requests to do so.
9#
10# The purpose of this module is that applications using pySerial can connect to
11# TCP/IP to serial port converters that do not support RFC 2217.
12#
13# (C) 2001-2011 Chris Liechti <cliechti@gmx.net>
14# this is distributed under a free software license, see license.txt
15#
16# URL format:    socket://<host>:<port>[/option[/option...]]
17# options:
18# - "debug" print diagnostic messages
19
20from serial.serialutil import *
21import time
22import socket
23import logging
24
25# map log level names to constants. used in fromURL()
26LOGGER_LEVELS = {
27    'debug': logging.DEBUG,
28    'info': logging.INFO,
29    'warning': logging.WARNING,
30    'error': logging.ERROR,
31    }
32
33POLL_TIMEOUT = 2
34
35class SocketSerial(SerialBase):
36    """Serial port implementation for plain sockets."""
37
38    BAUDRATES = (50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
39                 9600, 19200, 38400, 57600, 115200)
40
41    def open(self):
42        """Open port with current settings. This may throw a SerialException
43           if the port cannot be opened."""
44        self.logger = None
45        if self._port is None:
46            raise SerialException("Port must be configured before it can be used.")
47        if self._isOpen:
48            raise SerialException("Port is already open.")
49        try:
50            # XXX in future replace with create_connection (py >=2.6)
51            self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
52            self._socket.connect(self.fromURL(self.portstr))
53        except Exception, msg:
54            self._socket = None
55            raise SerialException("Could not open port %s: %s" % (self.portstr, msg))
56
57        self._socket.settimeout(POLL_TIMEOUT) # used for write timeout support :/
58
59        # not that there anything to configure...
60        self._reconfigurePort()
61        # all things set up get, now a clean start
62        self._isOpen = True
63        if not self._rtscts:
64            self.setRTS(True)
65            self.setDTR(True)
66        self.flushInput()
67        self.flushOutput()
68
69    def _reconfigurePort(self):
70        """Set communication parameters on opened port. for the socket://
71        protocol all settings are ignored!"""
72        if self._socket is None:
73            raise SerialException("Can only operate on open ports")
74        if self.logger:
75            self.logger.info('ignored port configuration change')
76
77    def close(self):
78        """Close port"""
79        if self._isOpen:
80            if self._socket:
81                try:
82                    self._socket.shutdown(socket.SHUT_RDWR)
83                    self._socket.close()
84                except:
85                    # ignore errors.
86                    pass
87                self._socket = None
88            self._isOpen = False
89            # in case of quick reconnects, give the server some time
90            time.sleep(0.3)
91
92    def makeDeviceName(self, port):
93        raise SerialException("there is no sensible way to turn numbers into URLs")
94
95    def fromURL(self, url):
96        """extract host and port from an URL string"""
97        if url.lower().startswith("socket://"): url = url[9:]
98        try:
99            # is there a "path" (our options)?
100            if '/' in url:
101                # cut away options
102                url, options = url.split('/', 1)
103                # process options now, directly altering self
104                for option in options.split('/'):
105                    if '=' in option:
106                        option, value = option.split('=', 1)
107                    else:
108                        value = None
109                    if option == 'logging':
110                        logging.basicConfig()   # XXX is that good to call it here?
111                        self.logger = logging.getLogger('pySerial.socket')
112                        self.logger.setLevel(LOGGER_LEVELS[value])
113                        self.logger.debug('enabled logging')
114                    else:
115                        raise ValueError('unknown option: %r' % (option,))
116            # get host and port
117            host, port = url.split(':', 1) # may raise ValueError because of unpacking
118            port = int(port)               # and this if it's not a number
119            if not 0 <= port < 65536: raise ValueError("port not in range 0...65535")
120        except ValueError, e:
121            raise SerialException('expected a string in the form "[rfc2217://]<host>:<port>[/option[/option...]]": %s' % e)
122        return (host, port)
123
124    #  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -
125
126    def inWaiting(self):
127        """Return the number of characters currently in the input buffer."""
128        if not self._isOpen: raise portNotOpenError
129        if self.logger:
130            # set this one to debug as the function could be called often...
131            self.logger.debug('WARNING: inWaiting returns dummy value')
132        return 0 # hmmm, see comment in read()
133
134    def read(self, size=1):
135        """Read size bytes from the serial port. If a timeout is set it may
136        return less characters as requested. With no timeout it will block
137        until the requested number of bytes is read."""
138        if not self._isOpen: raise portNotOpenError
139        data = bytearray()
140        if self._timeout is not None:
141            timeout = time.time() + self._timeout
142        else:
143            timeout = None
144        while len(data) < size and (timeout is None or time.time() < timeout):
145            try:
146                # an implementation with internal buffer would be better
147                # performing...
148                t = time.time()
149                block = self._socket.recv(size - len(data))
150                duration = time.time() - t
151                if block:
152                    data.extend(block)
153                else:
154                    # no data -> EOF (connection probably closed)
155                    break
156            except socket.timeout:
157                # just need to get out of recv from time to time to check if
158                # still alive
159                continue
160            except socket.error, e:
161                # connection fails -> terminate loop
162                raise SerialException('connection failed (%s)' % e)
163        return bytes(data)
164
165    def write(self, data):
166        """Output the given string over the serial port. Can block if the
167        connection is blocked. May raise SerialException if the connection is
168        closed."""
169        if not self._isOpen: raise portNotOpenError
170        try:
171            self._socket.sendall(to_bytes(data))
172        except socket.error, e:
173            # XXX what exception if socket connection fails
174            raise SerialException("socket connection failed: %s" % e)
175        return len(data)
176
177    def flushInput(self):
178        """Clear input buffer, discarding all that is in the buffer."""
179        if not self._isOpen: raise portNotOpenError
180        if self.logger:
181            self.logger.info('ignored flushInput')
182
183    def flushOutput(self):
184        """Clear output buffer, aborting the current output and
185        discarding all that is in the buffer."""
186        if not self._isOpen: raise portNotOpenError
187        if self.logger:
188            self.logger.info('ignored flushOutput')
189
190    def sendBreak(self, duration=0.25):
191        """Send break condition. Timed, returns to idle state after given
192        duration."""
193        if not self._isOpen: raise portNotOpenError
194        if self.logger:
195            self.logger.info('ignored sendBreak(%r)' % (duration,))
196
197    def setBreak(self, level=True):
198        """Set break: Controls TXD. When active, to transmitting is
199        possible."""
200        if not self._isOpen: raise portNotOpenError
201        if self.logger:
202            self.logger.info('ignored setBreak(%r)' % (level,))
203
204    def setRTS(self, level=True):
205        """Set terminal status line: Request To Send"""
206        if not self._isOpen: raise portNotOpenError
207        if self.logger:
208            self.logger.info('ignored setRTS(%r)' % (level,))
209
210    def setDTR(self, level=True):
211        """Set terminal status line: Data Terminal Ready"""
212        if not self._isOpen: raise portNotOpenError
213        if self.logger:
214            self.logger.info('ignored setDTR(%r)' % (level,))
215
216    def getCTS(self):
217        """Read terminal status line: Clear To Send"""
218        if not self._isOpen: raise portNotOpenError
219        if self.logger:
220            self.logger.info('returning dummy for getCTS()')
221        return True
222
223    def getDSR(self):
224        """Read terminal status line: Data Set Ready"""
225        if not self._isOpen: raise portNotOpenError
226        if self.logger:
227            self.logger.info('returning dummy for getDSR()')
228        return True
229
230    def getRI(self):
231        """Read terminal status line: Ring Indicator"""
232        if not self._isOpen: raise portNotOpenError
233        if self.logger:
234            self.logger.info('returning dummy for getRI()')
235        return False
236
237    def getCD(self):
238        """Read terminal status line: Carrier Detect"""
239        if not self._isOpen: raise portNotOpenError
240        if self.logger:
241            self.logger.info('returning dummy for getCD()')
242        return True
243
244    # - - - platform specific - - -
245    # None so far
246
247
248# assemble Serial class with the platform specific implementation and the base
249# for file-like behavior. for Python 2.6 and newer, that provide the new I/O
250# library, derive from io.RawIOBase
251try:
252    import io
253except ImportError:
254    # classic version with our own file-like emulation
255    class Serial(SocketSerial, FileLike):
256        pass
257else:
258    # io library present
259    class Serial(SocketSerial, io.RawIOBase):
260        pass
261
262
263# simple client test
264if __name__ == '__main__':
265    import sys
266    s = Serial('socket://localhost:7000')
267    sys.stdout.write('%s\n' % s)
268
269    sys.stdout.write("write...\n")
270    s.write("hello\n")
271    s.flush()
272    sys.stdout.write("read: %s\n" % s.read(5))
273
274    s.close()
275