blob: 44ad4eb44048a3f884c5d3b1e09f780d43419f23 [file] [log] [blame]
Diego Elio Pettenò8b24cbb2019-02-08 12:02:34 +00001#! 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
Chris Liechtie99bda32020-09-14 03:59:52 +020043from serial.serialutil import SerialBase, SerialException, PortNotOpenError, to_bytes, Timeout
Diego Elio Pettenò8b24cbb2019-02-08 12:02:34 +000044
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:
Chris Liechtie99bda32020-09-14 03:59:52 +0200188 raise PortNotOpenError()
Diego Elio Pettenò8b24cbb2019-02-08 12:02:34 +0000189 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:
Chris Liechtie99bda32020-09-14 03:59:52 +0200197 raise PortNotOpenError()
Diego Elio Pettenò8b24cbb2019-02-08 12:02:34 +0000198 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:
Chris Liechtie99bda32020-09-14 03:59:52 +0200203 raise PortNotOpenError()
Diego Elio Pettenò8b24cbb2019-02-08 12:02:34 +0000204
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:
Chris Liechtie99bda32020-09-14 03:59:52 +0200217 raise PortNotOpenError()
Diego Elio Pettenò8b24cbb2019-02-08 12:02:34 +0000218
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:
Chris Liechtie99bda32020-09-14 03:59:52 +0200237 raise PortNotOpenError()
Diego Elio Pettenò8b24cbb2019-02-08 12:02:34 +0000238 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