diff options
-rw-r--r-- | CHANGES.txt | 11 | ||||
-rw-r--r-- | documentation/pyserial_api.rst | 34 | ||||
-rw-r--r-- | examples/rfc2217_server.py | 2 | ||||
-rw-r--r-- | serial/__init__.py | 35 | ||||
-rw-r--r-- | test/handlers/__init__.py | 0 | ||||
-rw-r--r-- | test/handlers/protocol_test.py | 202 | ||||
-rw-r--r-- | test/test_url.py | 54 |
7 files changed, 322 insertions, 16 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index ec9eefc..d20212f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -397,3 +397,14 @@ Bugfixes (win32): - [Bug 2998169] Memory corruption at faster transmission speeds. (bug introduced in 2.5-rc1) + +Version 2.6 2011-nn-nn +--------------------------- +New Features: + +- Moved some of the examples to serial.tools so that they can be used + with ``python -m`` +- URL handers for ``serial_for_url`` are now imported dynamically. This allows + to add protocols w/o editing files. The list + ``serial.protocol_handler_packages`` can be used to add or remove user + packages with protocol handlers (see docs for details). diff --git a/documentation/pyserial_api.rst b/documentation/pyserial_api.rst index b2ba0d9..38aeb2c 100644 --- a/documentation/pyserial_api.rst +++ b/documentation/pyserial_api.rst @@ -698,8 +698,8 @@ Module version: .. versionadded:: 2.3 -Module functions -================ +Module functions and attributes +=============================== .. function:: device(number) @@ -732,20 +732,40 @@ Module functions .. versionadded:: 2.5 +.. attribute:: protocol_handler_packages + + This attribute is a list of package names (strings) that is searched for + protocol handlers. + + e.g. we want to support a URL ``foobar://``. A module + ``my_handlers.protocol_foobar`` is provided by the user:: + + serial.protocol_handler_packages.append("my_handlers") + s = serial.serial_for_url("foobar://") + + For an URL starting with ``XY://`` is the function :func:`serial_for_url` + attempts to import ``PACKAGE.protocol_XY`` with each candidate for + ``PACKAGE`` from this list. + + .. versionadded:: 2.6 + + .. function:: to_bytes(sequence) :param sequence: String or list of integers :returns: an instance of ``bytes`` - Convert a sequence to a bytes type. This is used to write code that is + Convert a sequence to a ``bytes`` type. This is used to write code that is compatible to Python 2.x and 3.x. - In Python versions prior 3.x, bytes is a subclass of str. They convert + In Python versions prior 3.x, ``bytes`` is a subclass of str. They convert ``str([17])`` to ``'[17]'`` instead of ``'\x11'`` so a simple - bytes(sequence) doesn't work for all versions of Python. + ``bytes(sequence)`` doesn't work for all versions of Python. This function is used internally and in the unit tests. + .. versionadded:: 2.5 + .. _URLs: @@ -762,7 +782,9 @@ Device names are also supported, e.g.: - ``/dev/ttyUSB0`` (Linux) - ``COM3`` (Windows) -(Future releases of pySerial might add more types). +Future releases of pySerial might add more types. Since pySerial 2.6 it is also +possible for the user to add protocol handlers using +:attr:`protocol_handler_packages`. ``rfc2217://`` Used to connect to :rfc:`2217` compatible servers. All serial port diff --git a/examples/rfc2217_server.py b/examples/rfc2217_server.py index 81fb520..069900a 100644 --- a/examples/rfc2217_server.py +++ b/examples/rfc2217_server.py @@ -163,7 +163,7 @@ it waits for the next connect. logging.info("Serving serial port: %s" % (ser.portstr,)) settings = ser.getSettingsDict() - # reset contol line as no _remote_ "terminal" has been connected yet + # reset control line as no _remote_ "terminal" has been connected yet ser.setDTR(False) ser.setRTS(False) diff --git a/serial/__init__.py b/serial/__init__.py index cc5e6dc..be1647c 100644 --- a/serial/__init__.py +++ b/serial/__init__.py @@ -6,7 +6,7 @@ # (C) 2001-2010 Chris Liechti <cliechti@gmx.net> # this is distributed under a free software license, see license.txt -VERSION = '2.5' +VERSION = '2.6-pre1' import sys @@ -24,11 +24,24 @@ else: else: raise Exception("Sorry: no implementation for your platform ('%s') available" % os.name) +protocol_handler_packages = [ + 'serial.urlhandler', + ] def serial_for_url(url, *args, **kwargs): - """Get a native, a RFC2217 or socket implementation of the Serial class, + """\ + Get a native, a RFC2217 or socket implementation of the Serial class, depending on port/url. The port is not opened when the keyword parameter - 'do_not_open' is true, by default it is.""" + 'do_not_open' is true, by default it is. + + The list of package names that is searched for protocol handlers is kept in + ``protocol_handler_packages`` + + e.g. we want to support a URL ``foobar://``. A module + ``my_handlers.protocol_foobar`` is provided by the user. Then + ``protocol_handler_packages.append("my_handlers")`` would extend the search + path so that ``serial_for_url("foobar://"))`` would work. + """ # check remove extra parameter to not confuse the Serial class do_open = 'do_not_open' not in kwargs or not kwargs['do_not_open'] if 'do_not_open' in kwargs: del kwargs['do_not_open'] @@ -43,13 +56,17 @@ def serial_for_url(url, *args, **kwargs): else: if '://' in url_nocase: protocol = url_nocase.split('://', 1)[0] - module_name = 'serial.urlhandler.protocol_%s' % (protocol,) - try: - handler_module = __import__(module_name) - except ImportError: - raise ValueError('invalid URL, protocol %r not known' % (protocol,)) + for package_name in protocol_handler_packages: + module_name = '%s.protocol_%s' % (package_name, protocol,) + try: + handler_module = __import__(module_name) + except ImportError: + pass + else: + klass = sys.modules[module_name].Serial + break else: - klass = sys.modules[module_name].Serial + raise ValueError('invalid URL, protocol %r not known' % (protocol,)) else: klass = Serial # 'native' implementation # instantiate and open when desired diff --git a/test/handlers/__init__.py b/test/handlers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/handlers/__init__.py diff --git a/test/handlers/protocol_test.py b/test/handlers/protocol_test.py new file mode 100644 index 0000000..57cdf58 --- /dev/null +++ b/test/handlers/protocol_test.py @@ -0,0 +1,202 @@ +#! python +# +# Python Serial Port Extension for Win32, Linux, BSD, Jython +# see __init__.py +# +# This module implements a URL dummy handler for serial_for_url. +# +# (C) 2011 Chris Liechti <cliechti@gmx.net> +# this is distributed under a free software license, see license.txt +# +# URL format: test:// + +from serial.serialutil import * +import time +import socket +import logging + +# map log level names to constants. used in fromURL() +LOGGER_LEVELS = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + } + +class DummySerial(SerialBase): + """Serial port implementation for plain sockets.""" + + 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.") + # not that there anything to configure... + self._reconfigurePort() + # all things set up get, now a clean start + self._isOpen = True + + def _reconfigurePort(self): + """Set communication parameters on opened port. for the test:// + protocol all settings are ignored!""" + if self.logger: + self.logger.info('ignored port configuration change') + + def close(self): + """Close port""" + if self._isOpen: + self._isOpen = False + + def makeDeviceName(self, port): + raise SerialException("there is no sensible way to turn numbers into URLs") + + def fromURL(self, url): + """extract host and port from an URL string""" + if url.lower().startswith("test://"): url = url[7:] + try: + # is there a "path" (our options)? + if '/' in url: + # cut away options + url, options = url.split('/', 1) + # process options now, directly altering self + for option in options.split('/'): + if '=' in option: + option, value = option.split('=', 1) + else: + value = None + if option == 'logging': + logging.basicConfig() # XXX is that good to call it here? + self.logger = logging.getLogger('pySerial.test') + self.logger.setLevel(LOGGER_LEVELS[value]) + self.logger.debug('enabled logging') + else: + raise ValueError('unknown option: %r' % (option,)) + except ValueError, e: + raise SerialException('expected a string in the form "[test://][option[/option...]]": %s' % e) + return (host, port) + + # - - - - - - - - - - - - - - - - - - - - - - - - + + def inWaiting(self): + """Return the number of characters currently in the input buffer.""" + if not self._isOpen: raise portNotOpenError + if self.logger: + # set this one to debug as the function could be called often... + self.logger.debug('WARNING: inWaiting returns dummy value') + return 0 # hmmm, see comment in read() + + 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._isOpen: raise portNotOpenError + data = '123' # dummy data + return bytes(data) + + def write(self, data): + """Output the given string over the serial port. Can block if the + connection is blocked. May raise SerialException if the connection is + closed.""" + if not self._isOpen: raise portNotOpenError + # nothing done + return len(data) + + def flushInput(self): + """Clear input buffer, discarding all that is in the buffer.""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('ignored flushInput') + + def flushOutput(self): + """Clear output buffer, aborting the current output and + discarding all that is in the buffer.""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('ignored flushOutput') + + def sendBreak(self, duration=0.25): + """Send break condition. Timed, returns to idle state after given + duration.""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('ignored sendBreak(%r)' % (duration,)) + + def setBreak(self, level=True): + """Set break: Controls TXD. When active, to transmitting is + possible.""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('ignored setBreak(%r)' % (level,)) + + def setRTS(self, level=True): + """Set terminal status line: Request To Send""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('ignored setRTS(%r)' % (level,)) + + def setDTR(self, level=True): + """Set terminal status line: Data Terminal Ready""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('ignored setDTR(%r)' % (level,)) + + def getCTS(self): + """Read terminal status line: Clear To Send""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('returning dummy for getCTS()') + return True + + def getDSR(self): + """Read terminal status line: Data Set Ready""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('returning dummy for getDSR()') + return True + + def getRI(self): + """Read terminal status line: Ring Indicator""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('returning dummy for getRI()') + return False + + def getCD(self): + """Read terminal status line: Carrier Detect""" + if not self._isOpen: raise portNotOpenError + if self.logger: + self.logger.info('returning dummy for getCD()') + return True + + # - - - platform specific - - - + # None so far + + +# assemble Serial class with the platform specific implementation and the base +# for file-like behavior. for Python 2.6 and newer, that provide the new I/O +# library, derive from io.RawIOBase +try: + import io +except ImportError: + # classic version with our own file-like emulation + class Serial(DummySerial, FileLike): + pass +else: + # io library present + class Serial(DummySerial, io.RawIOBase): + pass + + +# simple client test +if __name__ == '__main__': + import sys + s = Serial('test://logging=debug') + sys.stdout.write('%s\n' % s) + + sys.stdout.write("write...\n") + s.write("hello\n") + s.flush() + sys.stdout.write("read: %s\n" % s.read(5)) + + s.close() diff --git a/test/test_url.py b/test/test_url.py new file mode 100644 index 0000000..700bdbb --- /dev/null +++ b/test/test_url.py @@ -0,0 +1,54 @@ +#! /usr/bin/env python +# Python Serial Port Extension for Win32, Linux, BSD, Jython +# see __init__.py +# +# (C) 2001-2008 Chris Liechti <cliechti@gmx.net> +# this is distributed under a free software license, see license.txt + +"""\ +Some tests for the serial module. +Part of pySerial (http://pyserial.sf.net) (C)2001-2011 cliechti@gmx.net + +Intended to be run on different platforms, to ensure portability of +the code. + +Cover some of the aspects of serial_for_url and the extension mechanism. +""" + +import unittest +import time +import sys +import serial + + +class Test_URL(unittest.TestCase): + """Test serial_for_url""" + + def test_loop(self): + """loop interface""" + s = serial.serial_for_url('loop://', do_not_open=True) + + def test_bad_url(self): + """invalid protocol specified""" + self.failUnlessRaises(ValueError, serial.serial_for_url, "imnotknown://") + + def test_custom_url(self): + """custom protocol handlers""" + # it's unknown + self.failUnlessRaises(ValueError, serial.serial_for_url, "test://") + # add search path + serial.protocol_handler_packages.append('handlers') + # now it should work + s = serial.serial_for_url("test://") + # remove our handler again + serial.protocol_handler_packages.remove('handlers') + # so it should not work anymore + self.failUnlessRaises(ValueError, serial.serial_for_url, "test://") + + +if __name__ == '__main__': + import sys + sys.stdout.write(__doc__) + sys.argv[1:] = ['-v'] + # When this module is executed from the command-line, it runs all its tests + unittest.main() |