259 lines
8.3 KiB
Python
259 lines
8.3 KiB
Python
#! 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
|