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 <[email protected]> 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