1#! python
2#
3# Backend for Silicon Labs CP2110/4 HID-to-UART devices.
4#
5# This file is part of pySerial. https://github.com/pyserial/pyserial
6# (C) 2001-2015 Chris Liechti <cliechti@gmx.net>
7# (C) 2019 Google LLC
8#
9# SPDX-License-Identifier:    BSD-3-Clause
10
11# This backend implements support for HID-to-UART devices manufactured
12# by Silicon Labs and marketed as CP2110 and CP2114. The
13# implementation is (mostly) OS-independent and in userland. It relies
14# on cython-hidapi (https://github.com/trezor/cython-hidapi).
15
16# The HID-to-UART protocol implemented by CP2110/4 is described in the
17# AN434 document from Silicon Labs:
18# https://www.silabs.com/documents/public/application-notes/AN434-CP2110-4-Interface-Specification.pdf
19
20# TODO items:
21
22# - rtscts support is configured for hardware flow control, but the
23#   signaling is missing (AN434 suggests this is done through GPIO).
24# - Cancelling reads and writes is not supported.
25# - Baudrate validation is not implemented, as it depends on model and configuration.
26
27import struct
28import threading
29
30try:
31    import urlparse
32except ImportError:
33    import urllib.parse as urlparse
34
35try:
36    import Queue
37except ImportError:
38    import queue as Queue
39
40import hid  # hidapi
41
42import serial
43from serial.serialutil import SerialBase, SerialException, PortNotOpenError, to_bytes, Timeout
44
45
46# Report IDs and related constant
47_REPORT_GETSET_UART_ENABLE = 0x41
48_DISABLE_UART = 0x00
49_ENABLE_UART = 0x01
50
51_REPORT_SET_PURGE_FIFOS = 0x43
52_PURGE_TX_FIFO = 0x01
53_PURGE_RX_FIFO = 0x02
54
55_REPORT_GETSET_UART_CONFIG = 0x50
56
57_REPORT_SET_TRANSMIT_LINE_BREAK = 0x51
58_REPORT_SET_STOP_LINE_BREAK = 0x52
59
60
61class Serial(SerialBase):
62    # This is not quite correct. AN343 specifies that the minimum
63    # baudrate is different between CP2110 and CP2114, and it's halved
64    # when using non-8-bit symbols.
65    BAUDRATES = (300, 375, 600, 1200, 1800, 2400, 4800, 9600, 19200,
66                 38400, 57600, 115200, 230400, 460800, 500000, 576000,
67                 921600, 1000000)
68
69    def __init__(self, *args, **kwargs):
70        self._hid_handle = None
71        self._read_buffer = None
72        self._thread = None
73        super(Serial, self).__init__(*args, **kwargs)
74
75    def open(self):
76        if self._port is None:
77            raise SerialException("Port must be configured before it can be used.")
78        if self.is_open:
79            raise SerialException("Port is already open.")
80
81        self._read_buffer = Queue.Queue()
82
83        self._hid_handle = hid.device()
84        try:
85            portpath = self.from_url(self.portstr)
86            self._hid_handle.open_path(portpath)
87        except OSError as msg:
88            raise SerialException(msg.errno, "could not open port {}: {}".format(self._port, msg))
89
90        try:
91            self._reconfigure_port()
92        except:
93            try:
94                self._hid_handle.close()
95            except:
96                pass
97            self._hid_handle = None
98            raise
99        else:
100            self.is_open = True
101            self._thread = threading.Thread(target=self._hid_read_loop)
102            self._thread.setDaemon(True)
103            self._thread.setName('pySerial CP2110 reader thread for {}'.format(self._port))
104            self._thread.start()
105
106    def from_url(self, url):
107        parts = urlparse.urlsplit(url)
108        if parts.scheme != "cp2110":
109            raise SerialException(
110                'expected a string in the forms '
111                '"cp2110:///dev/hidraw9" or "cp2110://0001:0023:00": '
112                'not starting with cp2110:// {{!r}}'.format(parts.scheme))
113        if parts.netloc:  # cp2100://BUS:DEVICE:ENDPOINT, for libusb
114            return parts.netloc.encode('utf-8')
115        return parts.path.encode('utf-8')
116
117    def close(self):
118        self.is_open = False
119        if self._thread:
120            self._thread.join(1)  # read timeout is 0.1
121            self._thread = None
122        self._hid_handle.close()
123        self._hid_handle = None
124
125    def _reconfigure_port(self):
126        parity_value = None
127        if self._parity == serial.PARITY_NONE:
128            parity_value = 0x00
129        elif self._parity == serial.PARITY_ODD:
130            parity_value = 0x01
131        elif self._parity == serial.PARITY_EVEN:
132            parity_value = 0x02
133        elif self._parity == serial.PARITY_MARK:
134            parity_value = 0x03
135        elif self._parity == serial.PARITY_SPACE:
136            parity_value = 0x04
137        else:
138            raise ValueError('Invalid parity: {!r}'.format(self._parity))
139
140        if self.rtscts:
141            flow_control_value = 0x01
142        else:
143            flow_control_value = 0x00
144
145        data_bits_value = None
146        if self._bytesize == 5:
147            data_bits_value = 0x00
148        elif self._bytesize == 6:
149            data_bits_value = 0x01
150        elif self._bytesize == 7:
151            data_bits_value = 0x02
152        elif self._bytesize == 8:
153            data_bits_value = 0x03
154        else:
155            raise ValueError('Invalid char len: {!r}'.format(self._bytesize))
156
157        stop_bits_value = None
158        if self._stopbits == serial.STOPBITS_ONE:
159            stop_bits_value = 0x00
160        elif self._stopbits == serial.STOPBITS_ONE_POINT_FIVE:
161            stop_bits_value = 0x01
162        elif self._stopbits == serial.STOPBITS_TWO:
163            stop_bits_value = 0x01
164        else:
165            raise ValueError('Invalid stop bit specification: {!r}'.format(self._stopbits))
166
167        configuration_report = struct.pack(
168            '>BLBBBB',
169            _REPORT_GETSET_UART_CONFIG,
170            self._baudrate,
171            parity_value,
172            flow_control_value,
173            data_bits_value,
174            stop_bits_value)
175
176        self._hid_handle.send_feature_report(configuration_report)
177
178        self._hid_handle.send_feature_report(
179            bytes((_REPORT_GETSET_UART_ENABLE, _ENABLE_UART)))
180        self._update_break_state()
181
182    @property
183    def in_waiting(self):
184        return self._read_buffer.qsize()
185
186    def reset_input_buffer(self):
187        if not self.is_open:
188            raise PortNotOpenError()
189        self._hid_handle.send_feature_report(
190            bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_RX_FIFO)))
191        # empty read buffer
192        while self._read_buffer.qsize():
193            self._read_buffer.get(False)
194
195    def reset_output_buffer(self):
196        if not self.is_open:
197            raise PortNotOpenError()
198        self._hid_handle.send_feature_report(
199            bytes((_REPORT_SET_PURGE_FIFOS, _PURGE_TX_FIFO)))
200
201    def _update_break_state(self):
202        if not self._hid_handle:
203            raise PortNotOpenError()
204
205        if self._break_state:
206            self._hid_handle.send_feature_report(
207                bytes((_REPORT_SET_TRANSMIT_LINE_BREAK, 0)))
208        else:
209            # Note that while AN434 states "There are no data bytes in
210            # the payload other than the Report ID", either hidapi or
211            # Linux does not seem to send the report otherwise.
212            self._hid_handle.send_feature_report(
213                bytes((_REPORT_SET_STOP_LINE_BREAK, 0)))
214
215    def read(self, size=1):
216        if not self.is_open:
217            raise PortNotOpenError()
218
219        data = bytearray()
220        try:
221            timeout = Timeout(self._timeout)
222            while len(data) < size:
223                if self._thread is None:
224                    raise SerialException('connection failed (reader thread died)')
225                buf = self._read_buffer.get(True, timeout.time_left())
226                if buf is None:
227                    return bytes(data)
228                data += buf
229                if timeout.expired():
230                    break
231        except Queue.Empty:  # -> timeout
232            pass
233        return bytes(data)
234
235    def write(self, data):
236        if not self.is_open:
237            raise PortNotOpenError()
238        data = to_bytes(data)
239        tx_len = len(data)
240        while tx_len > 0:
241            to_be_sent = min(tx_len, 0x3F)
242            report = to_bytes([to_be_sent]) + data[:to_be_sent]
243            self._hid_handle.write(report)
244
245            data = data[to_be_sent:]
246            tx_len = len(data)
247
248    def _hid_read_loop(self):
249        try:
250            while self.is_open:
251                data = self._hid_handle.read(64, timeout_ms=100)
252                if not data:
253                    continue
254                data_len = data.pop(0)
255                assert data_len == len(data)
256                self._read_buffer.put(bytearray(data))
257        finally:
258            self._thread = None
259