Merge pull request #351 from cefn/master

win32: Working CMD.exe terminal using Windows 10 ANSI support
diff --git a/.gitignore b/.gitignore
index b33f61a..fb44ebb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@
 *.egg-info
 
 /MANIFEST
+
+.idea
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 6792d62..ff48704 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,8 +5,6 @@
 
 python:
   - 2.7
-  - 3.2
-  - 3.3
   - 3.4
   - 3.5
   - 3.6
diff --git a/documentation/pyserial_api.rst b/documentation/pyserial_api.rst
index 2045244..efd82c1 100644
--- a/documentation/pyserial_api.rst
+++ b/documentation/pyserial_api.rst
@@ -36,7 +36,7 @@
             :const:`STOPBITS_TWO`
 
         :param float timeout:
-            Set a read timeout value.
+            Set a read timeout value in seconds.
 
         :param bool xonxoff:
             Enable software flow control.
@@ -48,7 +48,7 @@
             Enable hardware (DSR/DTR) flow control.
 
         :param float write_timeout:
-            Set a write timeout value.
+            Set a write timeout value in seconds.
 
         :param float inter_byte_timeout:
             Inter-character timeout, :const:`None` to disable (default).
@@ -157,6 +157,22 @@
             Returns an instance of :class:`bytes` when available (Python 2.6
             and newer) and :class:`str` otherwise.
 
+    .. method:: read_until(expected=LF, size=None)
+
+        :param expected: The byte string to search for.
+        :param size: Number of bytes to read.
+        :return: Bytes read from the port.
+        :rtype: bytes
+
+        Read until an expected sequence is found ('\\n' by default), the size
+        is exceeded or until timeout occurs. 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.
+
+        .. versionchanged:: 2.5
+            Returns an instance of :class:`bytes` when available (Python 2.6
+            and newer) and :class:`str` otherwise.
+
     .. method:: write(data)
 
         :param data: Data to send.
@@ -168,7 +184,7 @@
 
         Write the bytes *data* to the port. This should be of type ``bytes``
         (or compatible such as ``bytearray`` or ``memoryview``). Unicode
-        strings must be encoded (e.g. ``'hello'.encode('utf-8'``).
+        strings must be encoded (e.g. ``'hello'.encode('utf-8')``.
 
     .. versionchanged:: 2.5
             Accepts instances of :class:`bytes` and :class:`bytearray` when
@@ -221,7 +237,7 @@
 
     .. method:: send_break(duration=0.25)
 
-        :param float duration: Time to activate the BREAK condition.
+        :param float duration: Time in seconds, to activate the BREAK condition.
 
         Send break condition. Timed, returns to idle state after given
         duration.
diff --git a/documentation/url_handlers.rst b/documentation/url_handlers.rst
index b4f0da7..42a53fa 100644
--- a/documentation/url_handlers.rst
+++ b/documentation/url_handlers.rst
@@ -16,6 +16,7 @@
 - ``hwgrep://<regexp>[&skip_busy][&n=N]``
 - ``spy://port[?option[=value][&option[=value]]]``
 - ``alt://port?class=<classname>``
+- ``cp2110://<bus>:<dev>:<if>``
 
 .. versionchanged:: 3.0 Options are specified with ``?`` and ``&`` instead of ``/``
 
@@ -48,7 +49,7 @@
 
 - ``timeout=<value>``: Change network timeout (default 3 seconds). This is
   useful when the server takes a little more time to send its answers. The
-  timeout applies to the initial Telnet / :rfc:`2271` negotiation as well
+  timeout applies to the initial Telnet / :rfc:`2217` negotiation as well
   as changing port settings or control line change commands.
 
 - ``logging={debug|info|warning|error}``: Prints diagnostic messages (not
@@ -235,6 +236,21 @@
 
 .. versionadded:: 3.0
 
+``cp2110://``
+=============
+
+This backend implements support for HID-to-UART devices manufactured by Silicon
+Labs and marketed as CP2110 and CP2114. The implementation is (mostly)
+OS-independent and in userland. It relies on `cython-hidapi`_.
+
+.. _cython-hidapi: https://github.com/trezor/cython-hidapi
+
+Examples::
+
+    cp2110://0001:004a:00
+    cp2110://0002:0077:00
+
+.. versionadded:: 3.5
 
 Examples
 ========
@@ -247,5 +263,5 @@
 - ``hwgrep://0451:f432`` (USB VID:PID)
 - ``spy://COM54?file=log.txt``
 - ``alt:///dev/ttyUSB0?class=PosixPollSerial``
-
+- ``cp2110://0001:004a:00``
 
diff --git a/examples/tcp_serial_redirect.py b/examples/tcp_serial_redirect.py
index 53dc0ad..ae7fe2d 100755
--- a/examples/tcp_serial_redirect.py
+++ b/examples/tcp_serial_redirect.py
@@ -66,6 +66,13 @@
     group = parser.add_argument_group('serial port')
 
     group.add_argument(
+        "--bytesize",
+        choices=[5, 6, 7, 8],
+        type=int,
+        help="set bytesize, one of {5 6 7 8}, default: 8",
+        default=8)
+
+    group.add_argument(
         "--parity",
         choices=['N', 'E', 'O', 'S', 'M'],
         type=lambda c: c.upper(),
@@ -73,6 +80,13 @@
         default='N')
 
     group.add_argument(
+        "--stopbits",
+        choices=[1, 1.5, 2],
+        type=float,
+        help="set stopbits, one of {1 1.5 2}, default: 1",
+        default=1)
+
+    group.add_argument(
         '--rtscts',
         action='store_true',
         help='enable RTS/CTS flow control (default off)',
@@ -117,7 +131,9 @@
     # connect to serial port
     ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True)
     ser.baudrate = args.BAUDRATE
+    ser.bytesize = args.bytesize
     ser.parity = args.parity
+    ser.stopbits = args.stopbits
     ser.rtscts = args.rtscts
     ser.xonxoff = args.xonxoff
 
diff --git a/serial/__init__.py b/serial/__init__.py
index dcd7c12..afd63a6 100644
--- a/serial/__init__.py
+++ b/serial/__init__.py
@@ -15,7 +15,7 @@
 from serial.serialutil import *
 #~ SerialBase, SerialException, to_bytes, iterbytes
 
-__version__ = '3.4'
+__version__ = '3.4.1'
 
 VERSION = __version__
 
diff --git a/serial/rfc2217.py b/serial/rfc2217.py
index d962c1e..2ae188e 100644
--- a/serial/rfc2217.py
+++ b/serial/rfc2217.py
@@ -76,7 +76,7 @@
 
 import serial
 from serial.serialutil import SerialBase, SerialException, to_bytes, \
-    iterbytes, portNotOpenError, Timeout
+    iterbytes, PortNotOpenError, Timeout
 
 # port string is expected to be something like this:
 # rfc2217://host:port
@@ -483,7 +483,7 @@
             if self.logger:
                 self.logger.info("Negotiated options: {}".format(self._telnet_options))
 
-            # fine, go on, set RFC 2271 specific things
+            # fine, go on, set RFC 2217 specific things
             self._reconfigure_port()
             # all things set up get, now a clean start
             if not self._dsrdtr:
@@ -598,7 +598,7 @@
     def in_waiting(self):
         """Return the number of bytes currently in the input buffer."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return self._read_buffer.qsize()
 
     def read(self, size=1):
@@ -608,12 +608,12 @@
         until the requested number of bytes is read.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         data = bytearray()
         try:
             timeout = Timeout(self._timeout)
             while len(data) < size:
-                if self._thread is None:
+                if self._thread is None or not self._thread.is_alive():
                     raise SerialException('connection failed (reader thread died)')
                 buf = self._read_buffer.get(True, timeout.time_left())
                 if buf is None:
@@ -632,7 +632,7 @@
         closed.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         with self._write_lock:
             try:
                 self._socket.sendall(to_bytes(data).replace(IAC, IAC_DOUBLED))
@@ -643,7 +643,7 @@
     def reset_input_buffer(self):
         """Clear input buffer, discarding all that is in the buffer."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.rfc2217_send_purge(PURGE_RECEIVE_BUFFER)
         # empty read buffer
         while self._read_buffer.qsize():
@@ -655,7 +655,7 @@
         discarding all that is in the buffer.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.rfc2217_send_purge(PURGE_TRANSMIT_BUFFER)
 
     def _update_break_state(self):
@@ -664,7 +664,7 @@
         possible.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('set BREAK to {}'.format('active' if self._break_state else 'inactive'))
         if self._break_state:
@@ -675,7 +675,7 @@
     def _update_rts_state(self):
         """Set terminal status line: Request To Send."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('set RTS to {}'.format('active' if self._rts_state else 'inactive'))
         if self._rts_state:
@@ -686,7 +686,7 @@
     def _update_dtr_state(self):
         """Set terminal status line: Data Terminal Ready."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('set DTR to {}'.format('active' if self._dtr_state else 'inactive'))
         if self._dtr_state:
@@ -698,28 +698,28 @@
     def cts(self):
         """Read terminal status line: Clear To Send."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return bool(self.get_modem_state() & MODEMSTATE_MASK_CTS)
 
     @property
     def dsr(self):
         """Read terminal status line: Data Set Ready."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return bool(self.get_modem_state() & MODEMSTATE_MASK_DSR)
 
     @property
     def ri(self):
         """Read terminal status line: Ring Indicator."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return bool(self.get_modem_state() & MODEMSTATE_MASK_RI)
 
     @property
     def cd(self):
         """Read terminal status line: Carrier Detect."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return bool(self.get_modem_state() & MODEMSTATE_MASK_CD)
 
     # - - - platform specific - - -
@@ -790,7 +790,6 @@
                         self._telnet_negotiate_option(telnet_command, byte)
                         mode = M_NORMAL
         finally:
-            self._thread = None
             if self.logger:
                 self.logger.debug("read thread terminated")
 
diff --git a/serial/serialcli.py b/serial/serialcli.py
index ddd0cdf..4614736 100644
--- a/serial/serialcli.py
+++ b/serial/serialcli.py
@@ -148,7 +148,7 @@
     def in_waiting(self):
         """Return the number of characters currently in the input buffer."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return self._port_handle.BytesToRead
 
     def read(self, size=1):
@@ -158,7 +158,7 @@
         until the requested number of bytes is read.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         # must use single byte reads as this is the only way to read
         # without applying encodings
         data = bytearray()
@@ -174,7 +174,7 @@
     def write(self, data):
         """Output the given string over the serial port."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         #~ if not isinstance(data, (bytes, bytearray)):
             #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
         try:
@@ -182,13 +182,13 @@
             # as this is the only one not applying encodings
             self._port_handle.Write(as_byte_array(data), 0, len(data))
         except System.TimeoutException:
-            raise writeTimeoutError
+            raise SerialTimeoutException('Write timeout')
         return len(data)
 
     def reset_input_buffer(self):
         """Clear input buffer, discarding all that is in the buffer."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self._port_handle.DiscardInBuffer()
 
     def reset_output_buffer(self):
@@ -197,7 +197,7 @@
         discarding all that is in the buffer.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self._port_handle.DiscardOutBuffer()
 
     def _update_break_state(self):
@@ -205,40 +205,40 @@
         Set break: Controls TXD. When active, to transmitting is possible.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self._port_handle.BreakState = bool(self._break_state)
 
     def _update_rts_state(self):
         """Set terminal status line: Request To Send"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self._port_handle.RtsEnable = bool(self._rts_state)
 
     def _update_dtr_state(self):
         """Set terminal status line: Data Terminal Ready"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self._port_handle.DtrEnable = bool(self._dtr_state)
 
     @property
     def cts(self):
         """Read terminal status line: Clear To Send"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return self._port_handle.CtsHolding
 
     @property
     def dsr(self):
         """Read terminal status line: Data Set Ready"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return self._port_handle.DsrHolding
 
     @property
     def ri(self):
         """Read terminal status line: Ring Indicator"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         #~ return self._port_handle.XXX
         return False  # XXX an error would be better
 
@@ -246,7 +246,7 @@
     def cd(self):
         """Read terminal status line: Carrier Detect"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return self._port_handle.CDHolding
 
     # - - platform specific - - - -
diff --git a/serial/serialjava.py b/serial/serialjava.py
index 9c920c5..0789a78 100644
--- a/serial/serialjava.py
+++ b/serial/serialjava.py
@@ -152,7 +152,7 @@
     def in_waiting(self):
         """Return the number of characters currently in the input buffer."""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return self._instream.available()
 
     def read(self, size=1):
@@ -162,7 +162,7 @@
         until the requested number of bytes is read.
         """
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         read = bytearray()
         if size > 0:
             while len(read) < size:
@@ -177,7 +177,7 @@
     def write(self, data):
         """Output the given string over the serial port."""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if not isinstance(data, (bytes, bytearray)):
             raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
         self._outstream.write(data)
@@ -186,7 +186,7 @@
     def reset_input_buffer(self):
         """Clear input buffer, discarding all that is in the buffer."""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self._instream.skip(self._instream.available())
 
     def reset_output_buffer(self):
@@ -195,57 +195,57 @@
         discarding all that is in the buffer.
         """
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self._outstream.flush()
 
     def send_break(self, duration=0.25):
         """Send break condition. Timed, returns to idle state after given duration."""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.sPort.sendBreak(duration*1000.0)
 
     def _update_break_state(self):
         """Set break: Controls TXD. When active, to transmitting is possible."""
         if self.fd is None:
-            raise portNotOpenError
+            raise PortNotOpenError()
         raise SerialException("The _update_break_state function is not implemented in java.")
 
     def _update_rts_state(self):
         """Set terminal status line: Request To Send"""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.sPort.setRTS(self._rts_state)
 
     def _update_dtr_state(self):
         """Set terminal status line: Data Terminal Ready"""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.sPort.setDTR(self._dtr_state)
 
     @property
     def cts(self):
         """Read terminal status line: Clear To Send"""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.sPort.isCTS()
 
     @property
     def dsr(self):
         """Read terminal status line: Data Set Ready"""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.sPort.isDSR()
 
     @property
     def ri(self):
         """Read terminal status line: Ring Indicator"""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.sPort.isRI()
 
     @property
     def cd(self):
         """Read terminal status line: Carrier Detect"""
         if not self.sPort:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.sPort.isCD()
diff --git a/serial/serialposix.py b/serial/serialposix.py
index 507e2fe..2f125c3 100644
--- a/serial/serialposix.py
+++ b/serial/serialposix.py
@@ -39,7 +39,7 @@
 
 import serial
 from serial.serialutil import SerialBase, SerialException, to_bytes, \
-    portNotOpenError, writeTimeoutError, Timeout
+    PortNotOpenError, SerialTimeoutException, Timeout
 
 
 class PlatformSpecificBase(object):
@@ -54,6 +54,15 @@
     def set_low_latency_mode(self, low_latency_settings):
         raise NotImplementedError('Low latency not supported on this platform')
 
+    def _update_break_state(self):
+        """\
+        Set break: Controls TXD. When active, no transmitting is possible.
+        """
+        if self._break_state:
+            fcntl.ioctl(self.fd, TIOCSBRK)
+        else:
+            fcntl.ioctl(self.fd, TIOCCBRK)
+    
 
 # some systems support an extra flag to enable the two in POSIX unsupported
 # paritiy settings for MARK and SPACE
@@ -205,6 +214,9 @@
 
     class PlatformSpecific(PlatformSpecificBase):
         osx_version = os.uname()[2].split('.')
+        TIOCSBRK = 0x2000747B # _IO('t', 123)
+        TIOCCBRK = 0x2000747A # _IO('t', 122)
+
         # Tiger or above can support arbitrary serial speeds
         if int(osx_version[0]) >= 8:
             def _set_special_baudrate(self, baudrate):
@@ -212,6 +224,15 @@
                 buf = array.array('i', [baudrate])
                 fcntl.ioctl(self.fd, IOSSIOSPEED, buf, 1)
 
+        def _update_break_state(self):
+            """\
+            Set break: Controls TXD. When active, no transmitting is possible.
+            """
+            if self._break_state:
+                fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
+            else:
+                fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
+
 elif plat[:3] == 'bsd' or \
      plat[:7] == 'freebsd' or \
      plat[:6] == 'netbsd' or \
@@ -227,6 +248,19 @@
         # a literal value.
         BAUDRATE_CONSTANTS = ReturnBaudrate()
 
+        TIOCSBRK = 0x2000747B # _IO('t', 123)
+        TIOCCBRK = 0x2000747A # _IO('t', 122)
+
+        
+        def _update_break_state(self):
+            """\
+            Set break: Controls TXD. When active, no transmitting is possible.
+            """
+            if self._break_state:
+                fcntl.ioctl(self.fd, PlatformSpecific.TIOCSBRK)
+            else:
+                fcntl.ioctl(self.fd, PlatformSpecific.TIOCCBRK)
+
 else:
     class PlatformSpecific(PlatformSpecificBase):
         pass
@@ -371,8 +405,15 @@
                 ispeed = ospeed = self.BAUDRATE_CONSTANTS[self._baudrate]
             except KeyError:
                 #~ raise ValueError('Invalid baud rate: %r' % self._baudrate)
-                # may need custom baud rate, it isn't in our list.
-                ispeed = ospeed = getattr(termios, 'B38400')
+
+                # See if BOTHER is defined for this platform; if it is, use
+                # this for a speed not defined in the baudrate constants list.
+                try:
+                    ispeed = ospeed = BOTHER
+                except NameError:
+                    # may need custom baud rate, it isn't in our list.
+                    ispeed = ospeed = getattr(termios, 'B38400')
+
                 try:
                     custom_baud = int(self._baudrate)  # store for later
                 except ValueError:
@@ -498,7 +539,7 @@
         until the requested number of bytes is read.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         read = bytearray()
         timeout = Timeout(self._timeout)
         while len(read) < size:
@@ -514,16 +555,6 @@
                 if not ready:
                     break   # timeout
                 buf = os.read(self.fd, size - len(read))
-                # read should always return some data as select reported it was
-                # ready to read when we get to this point.
-                if not buf:
-                    # Disconnected devices, at least on Linux, show the
-                    # behavior that they are always ready to read immediately
-                    # but reading returns nothing.
-                    raise SerialException(
-                        'device reports readiness to read but returned no data '
-                        '(device disconnected or multiple access on port?)')
-                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
@@ -536,6 +567,18 @@
                 # 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))
+            else:
+                # read should always return some data as select reported it was
+                # ready to read when we get to this point.
+                if not buf:
+                    # Disconnected devices, at least on Linux, show the
+                    # behavior that they are always ready to read immediately
+                    # but reading returns nothing.
+                    raise SerialException(
+                        'device reports readiness to read but returned no data '
+                        '(device disconnected or multiple access on port?)')
+                read.extend(buf)
+
             if timeout.expired():
                 break
         return bytes(read)
@@ -551,7 +594,7 @@
     def write(self, data):
         """Output the given byte string over the serial port."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         d = to_bytes(data)
         tx_len = length = len(d)
         timeout = Timeout(self._write_timeout)
@@ -566,13 +609,13 @@
                     # when timeout is set, use select to wait for being ready
                     # with the time left as timeout
                     if timeout.expired():
-                        raise writeTimeoutError
+                        raise SerialTimeoutException('Write timeout')
                     abort, ready, _ = select.select([self.pipe_abort_write_r], [self.fd], [], timeout.time_left())
                     if abort:
                         os.read(self.pipe_abort_write_r, 1000)
                         break
                     if not ready:
-                        raise writeTimeoutError
+                        raise SerialTimeoutException('Write timeout')
                 else:
                     assert timeout.time_left() is None
                     # wait for write operation
@@ -599,7 +642,7 @@
                 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 writeTimeoutError
+                raise SerialTimeoutException('Write timeout')
         return length - len(d)
 
     def flush(self):
@@ -608,13 +651,13 @@
         is written.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         termios.tcdrain(self.fd)
 
     def reset_input_buffer(self):
         """Clear input buffer, discarding all that is in the buffer."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         termios.tcflush(self.fd, termios.TCIFLUSH)
 
     def reset_output_buffer(self):
@@ -623,7 +666,7 @@
         that is in the buffer.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         termios.tcflush(self.fd, termios.TCOFLUSH)
 
     def send_break(self, duration=0.25):
@@ -632,18 +675,9 @@
         duration.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         termios.tcsendbreak(self.fd, int(duration / 0.25))
 
-    def _update_break_state(self):
-        """\
-        Set break: Controls TXD. When active, no transmitting is possible.
-        """
-        if self._break_state:
-            fcntl.ioctl(self.fd, TIOCSBRK)
-        else:
-            fcntl.ioctl(self.fd, TIOCCBRK)
-
     def _update_rts_state(self):
         """Set terminal status line: Request To Send"""
         if self._rts_state:
@@ -662,7 +696,7 @@
     def cts(self):
         """Read terminal status line: Clear To Send"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
         return struct.unpack('I', s)[0] & TIOCM_CTS != 0
 
@@ -670,7 +704,7 @@
     def dsr(self):
         """Read terminal status line: Data Set Ready"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
         return struct.unpack('I', s)[0] & TIOCM_DSR != 0
 
@@ -678,7 +712,7 @@
     def ri(self):
         """Read terminal status line: Ring Indicator"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
         return struct.unpack('I', s)[0] & TIOCM_RI != 0
 
@@ -686,7 +720,7 @@
     def cd(self):
         """Read terminal status line: Carrier Detect"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         s = fcntl.ioctl(self.fd, TIOCMGET, TIOCM_zero_str)
         return struct.unpack('I', s)[0] & TIOCM_CD != 0
 
@@ -705,7 +739,7 @@
         WARNING: this function is not portable to different platforms!
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         return self.fd
 
     def set_input_flow_control(self, enable=True):
@@ -715,7 +749,7 @@
         WARNING: this function is not portable to different platforms!
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if enable:
             termios.tcflow(self.fd, termios.TCION)
         else:
@@ -728,7 +762,7 @@
         WARNING: this function is not portable to different platforms!
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if enable:
             termios.tcflow(self.fd, termios.TCOON)
         else:
@@ -754,7 +788,7 @@
         until the requested number of bytes is read.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         read = bytearray()
         timeout = Timeout(self._timeout)
         poll = select.poll()
@@ -831,7 +865,7 @@
         until the requested number of bytes is read.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         read = bytearray()
         while len(read) < size:
             buf = os.read(self.fd, size - len(read))
diff --git a/serial/serialutil.py b/serial/serialutil.py
index e847a6a..8c5ccfe 100644
--- a/serial/serialutil.py
+++ b/serial/serialutil.py
@@ -97,8 +97,10 @@
     """Write timeouts give an exception"""
 
 
-writeTimeoutError = SerialTimeoutException('Write timeout')
-portNotOpenError = SerialException('Attempting to use a port that is not open')
+class PortNotOpenError(SerialException):
+    """Port is not open"""
+    def __init__(self):
+        super(PortNotOpenError, self).__init__('Attempting to use a port that is not open')
 
 
 class Timeout(object):
@@ -559,7 +561,7 @@
     # context manager
 
     def __enter__(self):
-        if not self.is_open:
+        if self._port is not None and not self.is_open:
             self.open()
         return self
 
@@ -574,7 +576,7 @@
         duration.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         self.break_condition = True
         time.sleep(duration)
         self.break_condition = False
@@ -649,19 +651,19 @@
         """
         return self.read(self.in_waiting)
 
-    def read_until(self, terminator=LF, size=None):
+    def read_until(self, expected=LF, size=None):
         """\
-        Read until a termination sequence is found ('\n' by default), the size
+        Read until an expected sequence is found ('\n' by default), the size
         is exceeded or until timeout occurs.
         """
-        lenterm = len(terminator)
+        lenterm = len(expected)
         line = bytearray()
         timeout = Timeout(self._timeout)
         while True:
             c = self.read(1)
             if c:
                 line += c
-                if line[-lenterm:] == terminator:
+                if line[-lenterm:] == expected:
                     break
                 if size is not None and len(line) >= size:
                     break
diff --git a/serial/serialwin32.py b/serial/serialwin32.py
index 74bfd05..54d3e12 100644
--- a/serial/serialwin32.py
+++ b/serial/serialwin32.py
@@ -17,7 +17,7 @@
 from serial import win32
 
 import serial
-from serial.serialutil import SerialBase, SerialException, to_bytes, portNotOpenError, writeTimeoutError
+from serial.serialutil import SerialBase, SerialException, to_bytes, PortNotOpenError, SerialTimeoutException
 
 
 class Serial(SerialBase):
@@ -266,7 +266,7 @@
         until the requested number of bytes is read.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if size > 0:
             win32.ResetEvent(self._overlapped_read.hEvent)
             flags = win32.DWORD()
@@ -303,7 +303,7 @@
     def write(self, data):
         """Output the given byte string over the serial port."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         #~ if not isinstance(data, (bytes, bytearray)):
             #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
         # convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview
@@ -322,7 +322,7 @@
                 if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED:
                     return n.value  # canceled IO is no error
                 if n.value != len(data):
-                    raise writeTimeoutError
+                    raise SerialTimeoutException('Write timeout')
                 return n.value
             else:
                 errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError()
@@ -351,7 +351,7 @@
     def reset_input_buffer(self):
         """Clear input buffer, discarding all that is in the buffer."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         win32.PurgeComm(self._port_handle, win32.PURGE_RXCLEAR | win32.PURGE_RXABORT)
 
     def reset_output_buffer(self):
@@ -360,13 +360,13 @@
         that is in the buffer.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         win32.PurgeComm(self._port_handle, win32.PURGE_TXCLEAR | win32.PURGE_TXABORT)
 
     def _update_break_state(self):
         """Set break: Controls TXD. When active, to transmitting is possible."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self._break_state:
             win32.SetCommBreak(self._port_handle)
         else:
@@ -388,7 +388,7 @@
 
     def _GetCommModemStatus(self):
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         stat = win32.DWORD()
         win32.GetCommModemStatus(self._port_handle, ctypes.byref(stat))
         return stat.value
@@ -418,7 +418,7 @@
     def set_buffer_size(self, rx_size=4096, tx_size=None):
         """\
         Recommend a buffer size to the driver (device driver can ignore this
-        value). Must be called before the port is opened.
+        value). Must be called after the port is opened.
         """
         if tx_size is None:
             tx_size = rx_size
@@ -432,7 +432,7 @@
         WARNING: this function is not portable to different platforms!
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if enable:
             win32.EscapeCommFunction(self._port_handle, win32.SETXON)
         else:
diff --git a/serial/threaded/__init__.py b/serial/threaded/__init__.py
index 9b8fa01..b8940b6 100644
--- a/serial/threaded/__init__.py
+++ b/serial/threaded/__init__.py
@@ -203,7 +203,7 @@
                 break
             else:
                 if data:
-                    # make a separated try-except for called used code
+                    # make a separated try-except for called user code
                     try:
                         self.protocol.data_received(data)
                     except Exception as e:
@@ -216,7 +216,7 @@
     def write(self, data):
         """Thread safe writing (uses lock)"""
         with self._lock:
-            self.serial.write(data)
+            return self.serial.write(data)
 
     def close(self):
         """Close the serial port and exit reader thread (uses lock)"""
diff --git a/serial/tools/list_ports_common.py b/serial/tools/list_ports_common.py
index 8a1b625..617f3dc 100644
--- a/serial/tools/list_ports_common.py
+++ b/serial/tools/list_ports_common.py
@@ -77,6 +77,9 @@
     def __eq__(self, other):
         return isinstance(other, ListPortInfo) and self.device == other.device
 
+    def __hash__(self):
+        return hash(self.device)
+
     def __lt__(self, other):
         if not isinstance(other, ListPortInfo):
             raise TypeError('unorderable types: {}() and {}()'.format(
diff --git a/serial/tools/list_ports_osx.py b/serial/tools/list_ports_osx.py
index f46a820..34a7f5a 100644
--- a/serial/tools/list_ports_osx.py
+++ b/serial/tools/list_ports_osx.py
@@ -35,25 +35,40 @@
 kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault")
 
 kCFStringEncodingMacRoman = 0
+kCFStringEncodingUTF8 = 0x08000100
+
+# defined in `IOKit/usb/USBSpec.h`
+kUSBVendorString = 'USB Vendor Name'
+kUSBSerialNumberString = 'USB Serial Number'
+
+# `io_name_t` defined as `typedef char io_name_t[128];`
+# in `device/device_types.h`
+io_name_size = 128
+
+# defined in `mach/kern_return.h`
+KERN_SUCCESS = 0
+# kern_return_t defined as `typedef int kern_return_t;` in `mach/i386/kern_return.h`
+kern_return_t = ctypes.c_int
 
 iokit.IOServiceMatching.restype = ctypes.c_void_p
 
 iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
-iokit.IOServiceGetMatchingServices.restype = ctypes.c_void_p
+iokit.IOServiceGetMatchingServices.restype = kern_return_t
 
 iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
+iokit.IOServiceGetMatchingServices.restype = kern_return_t
 
 iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32]
 iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p
 
 iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
-iokit.IORegistryEntryGetPath.restype = ctypes.c_void_p
+iokit.IORegistryEntryGetPath.restype = kern_return_t
 
 iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
-iokit.IORegistryEntryGetName.restype = ctypes.c_void_p
+iokit.IORegistryEntryGetName.restype = kern_return_t
 
 iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
-iokit.IOObjectGetClass.restype = ctypes.c_void_p
+iokit.IOObjectGetClass.restype = kern_return_t
 
 iokit.IOObjectRelease.argtypes = [ctypes.c_void_p]
 
@@ -64,6 +79,9 @@
 cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
 cf.CFStringGetCStringPtr.restype = ctypes.c_char_p
 
+cf.CFStringGetCString.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_uint32]
+cf.CFStringGetCString.restype = ctypes.c_bool
+
 cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p]
 cf.CFNumberGetValue.restype = ctypes.c_void_p
 
@@ -88,8 +106,8 @@
     """
     key = cf.CFStringCreateWithCString(
             kCFAllocatorDefault,
-            property.encode("mac_roman"),
-            kCFStringEncodingMacRoman)
+            property.encode("utf-8"),
+            kCFStringEncodingUTF8)
 
     CFContainer = iokit.IORegistryEntryCreateCFProperty(
             device_type,
@@ -101,7 +119,12 @@
     if CFContainer:
         output = cf.CFStringGetCStringPtr(CFContainer, 0)
         if output is not None:
-            output = output.decode('mac_roman')
+            output = output.decode('utf-8')
+        else:
+            buffer = ctypes.create_string_buffer(io_name_size);
+            success = cf.CFStringGetCString(CFContainer, ctypes.byref(buffer), io_name_size, kCFStringEncodingUTF8)
+            if success:
+                output = buffer.value.decode('utf-8')
         cf.CFRelease(CFContainer)
     return output
 
@@ -118,8 +141,8 @@
     """
     key = cf.CFStringCreateWithCString(
             kCFAllocatorDefault,
-            property.encode("mac_roman"),
-            kCFStringEncodingMacRoman)
+            property.encode("utf-8"),
+            kCFStringEncodingUTF8)
 
     CFContainer = iokit.IORegistryEntryCreateCFProperty(
             device_type,
@@ -137,12 +160,19 @@
         return number.value
     return None
 
-
 def IORegistryEntryGetName(device):
-    pathname = ctypes.create_string_buffer(100)  # TODO: Is this ok?
-    iokit.IOObjectGetClass(device, ctypes.byref(pathname))
-    return pathname.value
+    devicename = ctypes.create_string_buffer(io_name_size);
+    res = iokit.IORegistryEntryGetName(device, ctypes.byref(devicename))
+    if res != KERN_SUCCESS:
+        return None
+    # this works in python2 but may not be valid. Also I don't know if
+    # this encoding is guaranteed. It may be dependent on system locale.
+    return devicename.value.decode('utf-8')
 
+def IOObjectGetClass(device):
+    classname = ctypes.create_string_buffer(io_name_size)
+    iokit.IOObjectGetClass(device, ctypes.byref(classname))
+    return classname.value
 
 def GetParentDeviceByType(device, parent_type):
     """ Find the first parent of a device that implements the parent_type
@@ -150,15 +180,15 @@
         @return Pointer to the parent type, or None if it was not found.
     """
     # First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice.
-    parent_type = parent_type.encode('mac_roman')
-    while IORegistryEntryGetName(device) != parent_type:
+    parent_type = parent_type.encode('utf-8')
+    while IOObjectGetClass(device) != parent_type:
         parent = ctypes.c_void_p()
         response = iokit.IORegistryEntryGetParentEntry(
                 device,
-                "IOService".encode("mac_roman"),
+                "IOService".encode("utf-8"),
                 ctypes.byref(parent))
         # If we weren't able to find a parent for the device, we're done.
-        if response != 0:
+        if response != KERN_SUCCESS:
             return None
         device = parent
     return device
@@ -172,7 +202,7 @@
 
     iokit.IOServiceGetMatchingServices(
             kIOMasterPortDefault,
-            iokit.IOServiceMatching(service_type.encode('mac_roman')),
+            iokit.IOServiceMatching(service_type.encode('utf-8')),
             ctypes.byref(serial_port_iterator))
 
     services = []
@@ -246,9 +276,12 @@
                 # fetch some useful informations from properties
                 info.vid = get_int_property(usb_device, "idVendor", kCFNumberSInt16Type)
                 info.pid = get_int_property(usb_device, "idProduct", kCFNumberSInt16Type)
-                info.serial_number = get_string_property(usb_device, "USB Serial Number")
-                info.product = get_string_property(usb_device, "USB Product Name") or 'n/a'
-                info.manufacturer = get_string_property(usb_device, "USB Vendor Name")
+                info.serial_number = get_string_property(usb_device, kUSBSerialNumberString)
+                # We know this is a usb device, so the
+                # IORegistryEntryName should always be aliased to the
+                # usb product name string descriptor.
+                info.product = IORegistryEntryGetName(usb_device) or 'n/a'
+                info.manufacturer = get_string_property(usb_device, kUSBVendorString)
                 locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type)
                 info.location = location_to_string(locationID)
                 info.interface = search_for_locationID_in_interfaces(serial_interfaces, locationID)
diff --git a/serial/tools/list_ports_windows.py b/serial/tools/list_ports_windows.py
index 19b9499..2c530e8 100644
--- a/serial/tools/list_ports_windows.py
+++ b/serial/tools/list_ports_windows.py
@@ -118,11 +118,25 @@
 RegQueryValueEx.argtypes = [HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD]
 RegQueryValueEx.restype = LONG
 
+cfgmgr32 = ctypes.windll.LoadLibrary("Cfgmgr32")
+CM_Get_Parent = cfgmgr32.CM_Get_Parent
+CM_Get_Parent.argtypes = [PDWORD, DWORD, ULONG]
+CM_Get_Parent.restype = LONG
+
+CM_Get_Device_IDW = cfgmgr32.CM_Get_Device_IDW
+CM_Get_Device_IDW.argtypes = [DWORD, PTSTR, ULONG, ULONG]
+CM_Get_Device_IDW.restype = LONG
+
+CM_MapCrToWin32Err = cfgmgr32.CM_MapCrToWin32Err
+CM_MapCrToWin32Err.argtypes = [DWORD, DWORD]
+CM_MapCrToWin32Err.restype = DWORD
+
 
 DIGCF_PRESENT = 2
 DIGCF_DEVICEINTERFACE = 16
 INVALID_HANDLE_VALUE = 0
 ERROR_INSUFFICIENT_BUFFER = 122
+ERROR_NOT_FOUND = 1168
 SPDRP_HARDWAREID = 1
 SPDRP_FRIENDLYNAME = 12
 SPDRP_LOCATION_PATHS = 35
@@ -132,19 +146,110 @@
 KEY_READ = 0x20019
 
 
+MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH = 5
+
+
+def get_parent_serial_number(child_devinst, child_vid, child_pid, depth=0):
+    """ Get the serial number of the parent of a device.
+
+    Args:
+        child_devinst: The device instance handle to get the parent serial number of.
+        child_vid: The vendor ID of the child device.
+        child_pid: The product ID of the child device.
+        depth: The current iteration depth of the USB device tree.
+    """
+
+    # If the traversal depth is beyond the max, abandon attempting to find the serial number.
+    if depth > MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH:
+        return ''
+
+    # Get the parent device instance.
+    devinst = DWORD()
+    ret = CM_Get_Parent(ctypes.byref(devinst), child_devinst, 0)
+
+    if ret:
+        win_error = CM_MapCrToWin32Err(DWORD(ret), DWORD(0))
+
+        # If there is no parent available, the child was the root device. We cannot traverse
+        # further.
+        if win_error == ERROR_NOT_FOUND:
+            return ''
+
+        raise ctypes.WinError(win_error)
+
+    # Get the ID of the parent device and parse it for vendor ID, product ID, and serial number.
+    parentHardwareID = ctypes.create_unicode_buffer(250)
+
+    ret = CM_Get_Device_IDW(
+        devinst,
+        parentHardwareID,
+        ctypes.sizeof(parentHardwareID) - 1,
+        0)
+
+    if ret:
+        raise ctypes.WinError(CM_MapCrToWin32Err(DWORD(ret), DWORD(0)))
+
+    parentHardwareID_str = parentHardwareID.value
+    m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?',
+                  parentHardwareID_str,
+                  re.I)
+
+    vid = int(m.group(1), 16)
+    pid = None
+    serial_number = None
+    if m.group(3):
+        pid = int(m.group(3), 16)
+    if m.group(7):
+        serial_number = m.group(7)
+
+    # Check that the USB serial number only contains alpha-numeric characters. It may be a windows
+    # device ID (ephemeral ID).
+    if serial_number and not re.match(r'^\w+$', serial_number):
+        serial_number = None
+
+    if not vid or not pid:
+        # If pid and vid are not available at this device level, continue to the parent.
+        return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1)
+
+    if pid != child_pid or vid != child_vid:
+        # If the VID or PID has changed, we are no longer looking at the same physical device. The
+        # serial number is unknown.
+        return ''
+
+    # In this case, the vid and pid of the parent device are identical to the child. However, if
+    # there still isn't a serial number available, continue to the next parent.
+    if not serial_number:
+        return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1)
+
+    # Finally, the VID and PID are identical to the child and a serial number is present, so return
+    # it.
+    return serial_number
+
+
 def iterate_comports():
     """Return a generator that yields descriptions for serial ports"""
-    GUIDs = (GUID * 8)()  # so far only seen one used, so hope 8 are enough...
-    guids_size = DWORD()
+    PortsGUIDs = (GUID * 8)()  # so far only seen one used, so hope 8 are enough...
+    ports_guids_size = DWORD()
     if not SetupDiClassGuidsFromName(
             "Ports",
-            GUIDs,
-            ctypes.sizeof(GUIDs),
-            ctypes.byref(guids_size)):
+            PortsGUIDs,
+            ctypes.sizeof(PortsGUIDs),
+            ctypes.byref(ports_guids_size)):
         raise ctypes.WinError()
 
+    ModemsGUIDs = (GUID * 8)()  # so far only seen one used, so hope 8 are enough...
+    modems_guids_size = DWORD()
+    if not SetupDiClassGuidsFromName(
+            "Modem",
+            ModemsGUIDs,
+            ctypes.sizeof(ModemsGUIDs),
+            ctypes.byref(modems_guids_size)):
+        raise ctypes.WinError()
+
+    GUIDs = PortsGUIDs[:ports_guids_size.value] + ModemsGUIDs[:modems_guids_size.value]
+
     # repeat for all possible GUIDs
-    for index in range(guids_size.value):
+    for index in range(len(GUIDs)):
         bInterfaceNumber = None
         g_hdi = SetupDiGetClassDevs(
             ctypes.byref(GUIDs[index]),
@@ -213,15 +318,21 @@
             # in case of USB, make a more readable string, similar to that form
             # that we also generate on other platforms
             if szHardwareID_str.startswith('USB'):
-                m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(\w+))?', szHardwareID_str, re.I)
+                m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', szHardwareID_str, re.I)
                 if m:
                     info.vid = int(m.group(1), 16)
                     if m.group(3):
                         info.pid = int(m.group(3), 16)
                     if m.group(5):
                         bInterfaceNumber = int(m.group(5))
-                    if m.group(7):
+
+                    # Check that the USB serial number only contains alpha-numeric characters. It
+                    # may be a windows device ID (ephemeral ID) for composite devices.
+                    if m.group(7) and re.match(r'^\w+$', m.group(7)):
                         info.serial_number = m.group(7)
+                    else:
+                        info.serial_number = get_parent_serial_number(devinfo.DevInst, info.vid, info.pid)
+
                 # calculate a location string
                 loc_path_str = ctypes.create_unicode_buffer(250)
                 if SetupDiGetDeviceRegistryProperty(
diff --git a/serial/urlhandler/protocol_cp2110.py b/serial/urlhandler/protocol_cp2110.py
new file mode 100644
index 0000000..44ad4eb
--- /dev/null
+++ b/serial/urlhandler/protocol_cp2110.py
@@ -0,0 +1,258 @@
+#! python
+#
+# Backend for Silicon Labs CP2110/4 HID-to-UART devices.
+#
+# This file is part of pySerial. https://github.com/pyserial/pyserial
+# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
+# (C) 2019 Google LLC
+#
+# SPDX-License-Identifier:    BSD-3-Clause
+
+# This backend implements support for HID-to-UART devices manufactured
+# by Silicon Labs and marketed as CP2110 and CP2114. The
+# implementation is (mostly) OS-independent and in userland. It relies
+# on cython-hidapi (https://github.com/trezor/cython-hidapi).
+
+# The HID-to-UART protocol implemented by CP2110/4 is described in the
+# AN434 document from Silicon Labs:
+# https://www.silabs.com/documents/public/application-notes/AN434-CP2110-4-Interface-Specification.pdf
+
+# TODO items:
+
+# - rtscts support is configured for hardware flow control, but the
+#   signaling is missing (AN434 suggests this is done through GPIO).
+# - Cancelling reads and writes is not supported.
+# - Baudrate validation is not implemented, as it depends on model and configuration.
+
+import struct
+import threading
+
+try:
+    import urlparse
+except ImportError:
+    import urllib.parse as urlparse
+
+try:
+    import Queue
+except ImportError:
+    import queue as Queue
+
+import hid  # hidapi
+
+import serial
+from serial.serialutil import SerialBase, SerialException, PortNotOpenError, to_bytes, Timeout
+
+
+# Report IDs and related constant
+_REPORT_GETSET_UART_ENABLE = 0x41
+_DISABLE_UART = 0x00
+_ENABLE_UART = 0x01
+
+_REPORT_SET_PURGE_FIFOS = 0x43
+_PURGE_TX_FIFO = 0x01
+_PURGE_RX_FIFO = 0x02
+
+_REPORT_GETSET_UART_CONFIG = 0x50
+
+_REPORT_SET_TRANSMIT_LINE_BREAK = 0x51
+_REPORT_SET_STOP_LINE_BREAK = 0x52
+
+
+class Serial(SerialBase):
+    # This is not quite correct. AN343 specifies that the minimum
+    # baudrate is different between CP2110 and CP2114, and it's halved
+    # when using non-8-bit symbols.
+    BAUDRATES = (300, 375, 600, 1200, 1800, 2400, 4800, 9600, 19200,
+                 38400, 57600, 115200, 230400, 460800, 500000, 576000,
+                 921600, 1000000)
+
+    def __init__(self, *args, **kwargs):
+        self._hid_handle = None
+        self._read_buffer = None
+        self._thread = None
+        super(Serial, self).__init__(*args, **kwargs)
+
+    def open(self):
+        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.")
+
+        self._read_buffer = Queue.Queue()
+
+        self._hid_handle = hid.device()
+        try:
+            portpath = self.from_url(self.portstr)
+            self._hid_handle.open_path(portpath)
+        except OSError as msg:
+            raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
+
+        try:
+            self._reconfigure_port()
+        except:
+            try:
+                self._hid_handle.close()
+            except:
+                pass
+            self._hid_handle = None
+            raise
+        else:
+            self.is_open = True
+            self._thread = threading.Thread(target=self._hid_read_loop)
+            self._thread.setDaemon(True)
+            self._thread.setName('pySerial CP2110 reader thread for {}'.format(self._port))
+            self._thread.start()
+
+    def from_url(self, url):
+        parts = urlparse.urlsplit(url)
+        if parts.scheme != "cp2110":
+            raise SerialException(
+                'expected a string in the forms '
+                '"cp2110:///dev/hidraw9" or "cp2110://0001:0023:00": '
+                'not starting with cp2110:// {{!r}}'.format(parts.scheme))
+        if parts.netloc:  # cp2100://BUS:DEVICE:ENDPOINT, for libusb
+            return parts.netloc.encode('utf-8')
+        return parts.path.encode('utf-8')
+
+    def close(self):
+        self.is_open = False
+        if self._thread:
+            self._thread.join(1)  # read timeout is 0.1
+            self._thread = None
+        self._hid_handle.close()
+        self._hid_handle = None
+
+    def _reconfigure_port(self):
+        parity_value = None
+        if self._parity == serial.PARITY_NONE:
+            parity_value = 0x00
+        elif self._parity == serial.PARITY_ODD:
+            parity_value = 0x01
+        elif self._parity == serial.PARITY_EVEN:
+            parity_value = 0x02
+        elif self._parity == serial.PARITY_MARK:
+            parity_value = 0x03
+        elif self._parity == serial.PARITY_SPACE:
+            parity_value = 0x04
+        else:
+            raise ValueError('Invalid parity: {!r}'.format(self._parity))
+
+        if self.rtscts:
+            flow_control_value = 0x01
+        else:
+            flow_control_value = 0x00
+
+        data_bits_value = None
+        if self._bytesize == 5:
+            data_bits_value = 0x00
+        elif self._bytesize == 6:
+            data_bits_value = 0x01
+        elif self._bytesize == 7:
+            data_bits_value = 0x02
+        elif self._bytesize == 8:
+            data_bits_value = 0x03
+        else:
+            raise ValueError('Invalid char len: {!r}'.format(self._bytesize))
+
+        stop_bits_value = None
+        if self._stopbits == serial.STOPBITS_ONE:
+            stop_bits_value = 0x00
+        elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
+            stop_bits_value = 0x01
+        elif self._stopbits == serial.STOPBITS_TWO:
+            stop_bits_value = 0x01
+        else:
+            raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits))
+
+        configuration_report = struct.pack(
+            '>BLBBBB',
+            _REPORT_GETSET_UART_CONFIG,
+            self._baudrate,
+            parity_value,
+            flow_control_value,
+            data_bits_value,
+            stop_bits_value)
+
+        self._hid_handle.send_feature_report(configuration_report)
+
+        self._hid_handle.send_feature_report(
+            bytes((_REPORT_GETSET_UART_ENABLE, _ENABLE_UART)))
+        self._update_break_state()
+
+    @property
+    def in_waiting(self):
+        return self._read_buffer.qsize()
+
+    def reset_input_buffer(self):
+        if not self.is_open:
+            raise PortNotOpenError()
+        self._hid_handle.send_feature_report(
+            bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_RX_FIFO)))
+        # empty read buffer
+        while self._read_buffer.qsize():
+            self._read_buffer.get(False)
+
+    def reset_output_buffer(self):
+        if not self.is_open:
+            raise PortNotOpenError()
+        self._hid_handle.send_feature_report(
+            bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_TX_FIFO)))
+
+    def _update_break_state(self):
+        if not self._hid_handle:
+            raise PortNotOpenError()
+
+        if self._break_state:
+            self._hid_handle.send_feature_report(
+                bytes((_REPORT_SET_TRANSMIT_LINE_BREAK, 0)))
+        else:
+            # Note that while AN434 states "There are no data bytes in
+            # the payload other than the Report ID", either hidapi or
+            # Linux does not seem to send the report otherwise.
+            self._hid_handle.send_feature_report(
+                bytes((_REPORT_SET_STOP_LINE_BREAK, 0)))
+
+    def read(self, size=1):
+        if not self.is_open:
+            raise PortNotOpenError()
+
+        data = bytearray()
+        try:
+            timeout = Timeout(self._timeout)
+            while len(data) < size:
+                if self._thread is None:
+                    raise SerialException('connection failed (reader thread died)')
+                buf = self._read_buffer.get(True, timeout.time_left())
+                if buf is None:
+                    return bytes(data)
+                data += buf
+                if timeout.expired():
+                    break
+        except Queue.Empty:  # -> timeout
+            pass
+        return bytes(data)
+
+    def write(self, data):
+        if not self.is_open:
+            raise PortNotOpenError()
+        data = to_bytes(data)
+        tx_len = len(data)
+        while tx_len > 0:
+            to_be_sent = min(tx_len, 0x3F)
+            report = to_bytes([to_be_sent]) + data[:to_be_sent]
+            self._hid_handle.write(report)
+
+            data = data[to_be_sent:]
+            tx_len = len(data)
+
+    def _hid_read_loop(self):
+        try:
+            while self.is_open:
+                data = self._hid_handle.read(64, timeout_ms=100)
+                if not data:
+                    continue
+                data_len = data.pop(0)
+                assert data_len == len(data)
+                self._read_buffer.put(bytearray(data))
+        finally:
+            self._thread = None
diff --git a/serial/urlhandler/protocol_loop.py b/serial/urlhandler/protocol_loop.py
index 985e7a7..526583f 100644
--- a/serial/urlhandler/protocol_loop.py
+++ b/serial/urlhandler/protocol_loop.py
@@ -27,7 +27,7 @@
 except ImportError:
     import Queue as queue
 
-from serial.serialutil import SerialBase, SerialException, to_bytes, iterbytes, writeTimeoutError, portNotOpenError
+from serial.serialutil import SerialBase, SerialException, to_bytes, iterbytes, SerialTimeoutException, PortNotOpenError
 
 # map log level names to constants. used in from_url()
 LOGGER_LEVELS = {
@@ -127,7 +127,7 @@
     def in_waiting(self):
         """Return the number of bytes currently in the input buffer."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             # attention the logged value can differ from return value in
             # threaded environments...
@@ -141,7 +141,7 @@
         until the requested number of bytes is read.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self._timeout is not None and self._timeout != 0:
             timeout = time.time() + self._timeout
         else:
@@ -181,7 +181,7 @@
         """
         self._cancel_write = False
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         data = to_bytes(data)
         # calculate aprox time that would be used to send the data
         time_used_to_send = 10.0 * len(data) / self._baudrate
@@ -195,7 +195,7 @@
                 time_left -= 0.5
             if self._cancel_write:
                 return 0  # XXX
-            raise writeTimeoutError
+            raise SerialTimeoutException('Write timeout')
         for byte in iterbytes(data):
             self.queue.put(byte, timeout=self._write_timeout)
         return len(data)
@@ -203,7 +203,7 @@
     def reset_input_buffer(self):
         """Clear input buffer, discarding all that is in the buffer."""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('reset_input_buffer()')
         try:
@@ -218,7 +218,7 @@
         discarding all that is in the buffer.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('reset_output_buffer()')
         try:
@@ -249,7 +249,7 @@
     def cts(self):
         """Read terminal status line: Clear To Send"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('CTS -> state of RTS ({!r})'.format(self._rts_state))
         return self._rts_state
@@ -265,7 +265,7 @@
     def ri(self):
         """Read terminal status line: Ring Indicator"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('returning dummy for RI')
         return False
@@ -274,7 +274,7 @@
     def cd(self):
         """Read terminal status line: Carrier Detect"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('returning dummy for CD')
         return True
diff --git a/serial/urlhandler/protocol_rfc2217.py b/serial/urlhandler/protocol_rfc2217.py
index 8be310f..ebeec3a 100644
--- a/serial/urlhandler/protocol_rfc2217.py
+++ b/serial/urlhandler/protocol_rfc2217.py
@@ -1,6 +1,6 @@
 #! python
 #
-# This is a thin wrapper to load the rfc2271 implementation.
+# This is a thin wrapper to load the rfc2217 implementation.
 #
 # This file is part of pySerial. https://github.com/pyserial/pyserial
 # (C) 2011 Chris Liechti <cliechti@gmx.net>
diff --git a/serial/urlhandler/protocol_socket.py b/serial/urlhandler/protocol_socket.py
index 36cdf1f..2888467 100644
--- a/serial/urlhandler/protocol_socket.py
+++ b/serial/urlhandler/protocol_socket.py
@@ -29,7 +29,7 @@
     import urllib.parse as urlparse
 
 from serial.serialutil import SerialBase, SerialException, to_bytes, \
-    portNotOpenError, writeTimeoutError, Timeout
+    PortNotOpenError, SerialTimeoutException, Timeout
 
 # map log level names to constants. used in from_url()
 LOGGER_LEVELS = {
@@ -136,7 +136,7 @@
     def in_waiting(self):
         """Return the number of bytes currently in the input buffer."""
         if not self.is_open:
-            raise portNotOpenError
+            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)
@@ -152,7 +152,7 @@
         until the requested number of bytes is read.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         read = bytearray()
         timeout = Timeout(self._timeout)
         while len(read) < size:
@@ -193,7 +193,7 @@
         closed.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
 
         d = to_bytes(data)
         tx_len = length = len(d)
@@ -209,10 +209,10 @@
                     # when timeout is set, use select to wait for being ready
                     # with the time left as timeout
                     if timeout.expired():
-                        raise writeTimeoutError
+                        raise SerialTimeoutException('Write timeout')
                     _, ready, _ = select.select([], [self._socket], [], timeout.time_left())
                     if not ready:
-                        raise writeTimeoutError
+                        raise SerialTimeoutException('Write timeout')
                 else:
                     assert timeout.time_left() is None
                     # wait for write operation
@@ -236,20 +236,21 @@
                 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 writeTimeoutError
+                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
+            raise PortNotOpenError()
 
         # just use recv to remove input, while there is some
         ready = True
         while ready:
             ready, _, _ = select.select([self._socket], [], [], 0)
             try:
-                self._socket.recv(4096)
+                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
@@ -269,7 +270,7 @@
         discarding all that is in the buffer.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('ignored reset_output_buffer')
 
@@ -279,7 +280,7 @@
         duration.
         """
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('ignored send_break({!r})'.format(duration))
 
@@ -303,7 +304,7 @@
     def cts(self):
         """Read terminal status line: Clear To Send"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('returning dummy for cts')
         return True
@@ -312,7 +313,7 @@
     def dsr(self):
         """Read terminal status line: Data Set Ready"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('returning dummy for dsr')
         return True
@@ -321,7 +322,7 @@
     def ri(self):
         """Read terminal status line: Ring Indicator"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('returning dummy for ri')
         return False
@@ -330,7 +331,7 @@
     def cd(self):
         """Read terminal status line: Carrier Detect"""
         if not self.is_open:
-            raise portNotOpenError
+            raise PortNotOpenError()
         if self.logger:
             self.logger.info('returning dummy for cd)')
         return True
diff --git a/serial/win32.py b/serial/win32.py
index 08b6e67..157f470 100644
--- a/serial/win32.py
+++ b/serial/win32.py
@@ -181,6 +181,10 @@
 WaitForSingleObject.restype = DWORD
 WaitForSingleObject.argtypes = [HANDLE, DWORD]
 
+WaitCommEvent = _stdcall_libraries['kernel32'].WaitCommEvent
+WaitCommEvent.restype = BOOL
+WaitCommEvent.argtypes = [HANDLE, LPDWORD, LPOVERLAPPED]
+
 CancelIoEx = _stdcall_libraries['kernel32'].CancelIoEx
 CancelIoEx.restype = BOOL
 CancelIoEx.argtypes = [HANDLE, LPOVERLAPPED]
@@ -247,6 +251,12 @@
 PURGE_RXCLEAR = 8  # Variable c_int
 INFINITE = 0xFFFFFFFF
 
+CE_RXOVER = 0x0001
+CE_OVERRUN = 0x0002
+CE_RXPARITY = 0x0004
+CE_FRAME = 0x0008
+CE_BREAK = 0x0010
+
 
 class N11_OVERLAPPED4DOLLAR_48E(Union):
     pass
diff --git a/setup.py b/setup.py
index 6e8b586..ea53643 100644
--- a/setup.py
+++ b/setup.py
@@ -97,4 +97,7 @@
     ],
     platforms='any',
     scripts=['serial/tools/miniterm.py'],
+    extras_require = {
+        'cp2110': ['hidapi'],
+    },
 )
diff --git a/test/handlers/protocol_test.py b/test/handlers/protocol_test.py
index f2e572f..c0cffa6 100644
--- a/test/handlers/protocol_test.py
+++ b/test/handlers/protocol_test.py
@@ -80,7 +80,7 @@
 
     def inWaiting(self):
         """Return the number of characters currently in the input buffer."""
-        if not self._isOpen: raise portNotOpenError
+        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')
@@ -90,7 +90,7 @@
         """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
+        if not self._isOpen: raise PortNotOpenError()
         data = '123' # dummy data
         return bytes(data)
 
@@ -98,73 +98,73 @@
         """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
+        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 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 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 not self._isOpen: raise PortNotOpenError()
         if self.logger:
             self.logger.info('ignored sendBreak({!r})'.format(duration))
 
     def setBreak(self, level=True):
         """Set break: Controls TXD. When active, to transmitting is
         possible."""
-        if not self._isOpen: raise portNotOpenError
+        if not self._isOpen: raise PortNotOpenError()
         if self.logger:
             self.logger.info('ignored setBreak({!r})'.format(level))
 
     def setRTS(self, level=True):
         """Set terminal status line: Request To Send"""
-        if not self._isOpen: raise portNotOpenError
+        if not self._isOpen: raise PortNotOpenError()
         if self.logger:
             self.logger.info('ignored setRTS({!r})'.format(level))
 
     def setDTR(self, level=True):
         """Set terminal status line: Data Terminal Ready"""
-        if not self._isOpen: raise portNotOpenError
+        if not self._isOpen: raise PortNotOpenError()
         if self.logger:
             self.logger.info('ignored setDTR({!r})'.format(level))
 
     def getCTS(self):
         """Read terminal status line: Clear To Send"""
-        if not self._isOpen: raise portNotOpenError
+        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 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 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 not self._isOpen: raise PortNotOpenError()
         if self.logger:
             self.logger.info('returning dummy for getCD()')
         return True