summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt11
-rw-r--r--documentation/pyserial_api.rst34
-rw-r--r--examples/rfc2217_server.py2
-rw-r--r--serial/__init__.py35
-rw-r--r--test/handlers/__init__.py0
-rw-r--r--test/handlers/protocol_test.py202
-rw-r--r--test/test_url.py54
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()