123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- #! 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
|