From 066901d5bba192ff3e77f94f8eb6208b87ebba6d Mon Sep 17 00:00:00 2001 From: Quan Lin <36973354+jacklinquan@users.noreply.github.com> Date: Wed, 23 Jan 2019 15:36:41 +1100 Subject: [PATCH] Version 0.1.7 Flow control code added for FTDI driver. --- example/usbserial4a_ui_example.py | 34 ++- setup.py | 2 +- usbserial4a/__init__.py | 2 +- usbserial4a/ftdiserial4a.py | 435 ++++++++++++++++++++++++------ 4 files changed, 379 insertions(+), 94 deletions(-) diff --git a/example/usbserial4a_ui_example.py b/example/usbserial4a_ui_example.py index 841205b..d880bfb 100644 --- a/example/usbserial4a_ui_example.py +++ b/example/usbserial4a_ui_example.py @@ -90,6 +90,7 @@ class MainApp(App): self.device_name_list = [] self.serial_port = None self.read_thread = None + self.port_thread_lock = threading.Lock() super(MainApp, self).__init__(*args, **kwargs) def build(self): @@ -97,7 +98,8 @@ class MainApp(App): def on_stop(self): if self.serial_port: - self.serial_port.close() + with self.port_thread_lock: + self.serial_port.close() def on_btn_scan_release(self): self.uiDict['box_list'].clear_widgets() @@ -106,7 +108,8 @@ class MainApp(App): if platform == 'android': usb_device_list = usb.get_usb_device_list() self.device_name_list = [ - device.getDeviceName() for device in usb_device_list] + device.getDeviceName() for device in usb_device_list + ] else: usb_device_list = list_ports.comports() self.device_name_list = [port.device for port in usb_device_list] @@ -124,7 +127,8 @@ class MainApp(App): device = usb.get_usb_device(device_name) if not device: raise SerialException( - "Device {} not present!".format(device_name)) + "Device {} not present!".format(device_name) + ) if not usb.has_usb_permission(device): usb.request_usb_permission(device) return @@ -134,7 +138,8 @@ class MainApp(App): 8, 'N', 1, - timeout=1) + timeout=1 + ) else: self.serial_port = Serial( device_name, @@ -142,7 +147,8 @@ class MainApp(App): 8, 'N', 1, - timeout=1) + timeout=1 + ) if self.serial_port.is_open and not self.read_thread: self.read_thread = threading.Thread(target = self.read_msg_thread) @@ -156,24 +162,30 @@ class MainApp(App): data = bytes(self.uiDict['txtInput_write'].text + '\n') else: data = bytes( - (self.uiDict['txtInput_write'].text + '\n'), 'utf8') + (self.uiDict['txtInput_write'].text + '\n'), + 'utf8' + ) self.serial_port.write(data) self.uiDict['txtInput_read'].text += '[Sent]{}\n'.format( - self.uiDict['txtInput_write'].text) + self.uiDict['txtInput_write'].text + ) self.uiDict['txtInput_write'].text = '' def read_msg_thread(self): while True: - if not self.serial_port.is_open: - break try: - received_msg = self.serial_port.read() + with self.port_thread_lock: + if not self.serial_port.is_open: + break + received_msg = self.serial_port.read( + self.serial_port.in_waiting + ) if received_msg: msg = bytes(received_msg).decode('utf8') self.display_received_msg(msg) except Exception as ex: raise ex - + @mainthread def display_received_msg(self, msg): self.uiDict['txtInput_read'].text += msg diff --git a/setup.py b/setup.py index 713c1ed..8261ddb 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="usbserial4a", - version="0.1.6", + version="0.1.7", description="Python package for Kivy Android USB serial port.", long_description="https://github.com/jacklinquan/usbserial4a", long_description_content_type="text/markdown", diff --git a/usbserial4a/__init__.py b/usbserial4a/__init__.py index dad940b..a639506 100644 --- a/usbserial4a/__init__.py +++ b/usbserial4a/__init__.py @@ -9,4 +9,4 @@ Requires: kivy, pyjnius, pyserial, usb4a ''' # Project version -__version__ = '0.1.6' +__version__ = '0.1.7' diff --git a/usbserial4a/ftdiserial4a.py b/usbserial4a/ftdiserial4a.py index 362a883..e7fd13a 100644 --- a/usbserial4a/ftdiserial4a.py +++ b/usbserial4a/ftdiserial4a.py @@ -4,6 +4,8 @@ Classes: FtdiSerial(serial.serialutil.SerialBase) ''' +from struct import unpack +import time from serial.serialutil import SerialBase, SerialException, to_bytes, \ portNotOpenError, writeTimeoutError, Timeout from usb4a import usb @@ -14,6 +16,19 @@ class FtdiSerial(SerialBase): FtdiSerial extends serial.serialutil.SerialBase. It can be used in a similar way to serial.Serial from pyserial. ''' + # Modem status + MODEM_CTS = (1 << 4) # Clear to send + MODEM_DSR = (1 << 5) # Data set ready + MODEM_RI = (1 << 6) # Ring indicator + MODEM_RLSD = (1 << 7) # Carrier detect + MODEM_DR = (1 << 8) # Data ready + MODEM_OE = (1 << 9) # Overrun error + MODEM_PE = (1 << 10) # Parity error + MODEM_FE = (1 << 11) # Framing error + MODEM_BI = (1 << 12) # Break interrupt + MODEM_THRE = (1 << 13) # Transmitter holding register + MODEM_TEMT = (1 << 14) # Transmitter empty + MODEM_RCVE = (1 << 15) # Error in RCVR FIFO # Requests SIO_RESET = 0 # Reset the port @@ -52,6 +67,28 @@ class FtdiSerial(SerialBase): SIO_SET_RTS_HIGH = (SIO_SET_RTS_MASK | (SIO_SET_RTS_MASK << 8)) SIO_SET_RTS_LOW = (0x0 | (SIO_SET_RTS_MASK << 8)) + # Break type + BREAK_OFF, BREAK_ON = range(2) + + # cts: Clear to send + # dsr: Data set ready + # ri: Ring indicator + # dcd: Data carrier detect + # dr: Data ready + # oe: Overrun error + # pe: Parity error + # fe: Framing error + # bi: Break interrupt + # thre: Transmitter holding register + # temt: Transmitter empty + # err: Error in RCVR FIFO + MODEM_STATUS = [ + ('', '', '', '', 'cts', 'dsr', 'ri', 'dcd'), + ('dr', 'overrun', 'parity', 'framing', 'break', 'thre', 'txe', 'rcvr') + ] + + ERROR_BITS = (0x00, 0x8E) + # Clocks and baudrates BUS_CLOCK_BASE = 6.0E6 # 6 MHz BUS_CLOCK_HIGH = 30.0E6 # 30 MHz @@ -64,11 +101,13 @@ class FtdiSerial(SerialBase): FTDI_DEVICE_OUT_REQTYPE = usb.build_usb_control_request_type( usb.UsbConstants.USB_DIR_OUT, usb.UsbConstants.USB_TYPE_VENDOR, - usb.USB_RECIPIENT_DEVICE) + usb.USB_RECIPIENT_DEVICE + ) FTDI_DEVICE_IN_REQTYPE = usb.build_usb_control_request_type( usb.UsbConstants.USB_DIR_IN, usb.UsbConstants.USB_TYPE_VENDOR, - usb.USB_RECIPIENT_DEVICE) + usb.USB_RECIPIENT_DEVICE + ) DEFAULT_READ_BUFFER_SIZE = 1024 DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024 @@ -88,6 +127,7 @@ class FtdiSerial(SerialBase): self._control_endpoint = None self._read_endpoint = None self._write_endpoint = None + self._lineprop = 0 self._read_buffer = bytearray() super(FtdiSerial, self).__init__(*args, **kwargs) @@ -121,21 +161,28 @@ class FtdiSerial(SerialBase): self._interface = self._device.getInterface(i) if not self._connection.claimInterface( self._device.getInterface(i), - True): + True + ): raise SerialException("Could not claim interface {}.".format(i)) self._index = self._interface.getId() + 1 for i in range(self._interface.getEndpointCount()): ep = self._interface.getEndpoint(i) - if ((ep.getDirection() == usb.UsbConstants.USB_DIR_IN) and - (ep.getType() == usb.UsbConstants.USB_ENDPOINT_XFER_INT)): + if ( + (ep.getDirection() == usb.UsbConstants.USB_DIR_IN) \ + and (ep.getType() == usb.UsbConstants.USB_ENDPOINT_XFER_INT) + ): self._control_endpoint = ep - elif ((ep.getDirection() == usb.UsbConstants.USB_DIR_IN) and - (ep.getType() == usb.UsbConstants.USB_ENDPOINT_XFER_BULK)): + elif ( + (ep.getDirection() == usb.UsbConstants.USB_DIR_IN) \ + and (ep.getType() == usb.UsbConstants.USB_ENDPOINT_XFER_BULK) + ): self._read_endpoint = ep - elif ((ep.getDirection() == usb.UsbConstants.USB_DIR_OUT) and - (ep.getType() == usb.UsbConstants.USB_ENDPOINT_XFER_BULK)): + elif ( + (ep.getDirection() == usb.UsbConstants.USB_DIR_OUT) \ + and (ep.getType() == usb.UsbConstants.USB_ENDPOINT_XFER_BULK) + ): self._write_endpoint = ep # Check that all endpoints are good @@ -145,6 +192,22 @@ class FtdiSerial(SerialBase): self.is_open = True self._reconfigure_port() + def _reconfigure_port(self): + '''Reconfigure serial port parameters.''' + self._setParameters( + self.baudrate, + self.bytesize, + self.parity, + self.stopbits + ) + + if self._rtscts: + self._set_flowctrl('hw') + elif self._xonxoff: + self._set_flowctrl('sw') + else: + self._set_flowctrl('') + def close(self): '''Close the serial port.''' if self._connection: @@ -158,10 +221,13 @@ class FtdiSerial(SerialBase): result = self._ctrl_transfer_out( self.SIO_RESET, self.SIO_RESET_SIO, - 0) + self._index + ) if result != 0: raise SerialException("Reset failed: result={}".format(result)) + # - - - - - - - - - - - - - - - - - - - - - - - - + @property def in_waiting(self): '''Return the number of bytes currently in the input buffer. @@ -173,6 +239,14 @@ class FtdiSerial(SerialBase): self._read_buffer.extend(self._read()) return len(self._read_buffer) + @property + def out_waiting(self): + '''Return the number of bytes currently in the output buffer. + + Always return 0. + ''' + return 0 + def read(self, size=1): '''Read data from the serial port. @@ -212,18 +286,21 @@ class FtdiSerial(SerialBase): offset = 0 timeout = int( self._write_timeout * 1000 if self._write_timeout \ - else self.USB_WRITE_TIMEOUT_MILLIS) + else self.USB_WRITE_TIMEOUT_MILLIS + ) wrote = 0 while offset < len(data): data_length = min( len(data) - offset, - self.DEFAULT_WRITE_BUFFER_SIZE) + self.DEFAULT_WRITE_BUFFER_SIZE + ) buf = data[offset:offset + data_length] i = self._connection.bulkTransfer( self._write_endpoint, buf, data_length, - timeout) + timeout + ) if i <= 0: raise SerialException("Failed to write {}: {}".format(buf, i)) offset += data_length @@ -234,7 +311,151 @@ class FtdiSerial(SerialBase): '''Simply wait some time to allow all data to be written.''' pass - def purgeHwBuffers(self, purgeReadBuffers, purgeWriteBuffers): + def reset_input_buffer(self): + '''Clear input buffer, discarding all that is in the buffer.''' + if not self.is_open: + raise portNotOpenError + self._purgeHwBuffers(True, False) + + def reset_output_buffer(self): + '''\ + Clear output buffer, aborting the current output and discarding all + that is in the buffer. + ''' + if not self.is_open: + raise portNotOpenError + self._purgeHwBuffers(False, True) + + def send_break(self, duration=0.25): + '''Send break condition. + + Parameters: + duration (float): break time in seconds. + ''' + if not self.is_open: + raise portNotOpenError + self._set_break(True) + time.sleep(duration) + self._set_break(False) + + def _update_break_state(self): + '''Send break condition.''' + self._set_break(self._break_state) + + def _update_rts_state(self): + '''Set terminal status line: Request To Send.''' + self._set_rts(self._rts_state) + + def _update_dtr_state(self): + '''Set terminal status line: Data Terminal Ready.''' + self._set_dtr(self._dtr_state) + + @property + def cts(self): + '''Read terminal status line: Clear To Send.''' + status = self._poll_modem_status() + return bool(status & self.MODEM_CTS) + + @property + def dsr(self): + '''Read terminal status line: Data Set Ready.''' + status = self._poll_modem_status() + return bool(status & self.MODEM_DSR) + + @property + def ri(self): + '''Read terminal status line: Ring Indicator.''' + status = self._poll_modem_status() + return bool(status & self.MODEM_RI) + + @property + def cd(self): + '''Read terminal status line: Carrier Detect.''' + status = self._poll_modem_status() + return bool(status & self.MODEM_RLSD) + + # - - - - - - - - - - - - - - - - - - - - - - - - + + def _ctrl_transfer_out(self, request, value, index): + '''USB control transfer out. + + This function does the USB configuration job. + ''' + return self._connection.controlTransfer( + self.FTDI_DEVICE_OUT_REQTYPE, + request, + value, + index, + None, + 0, + self.USB_WRITE_TIMEOUT_MILLIS + ) + + def _ctrl_transfer_in(self, request, buf, length): + '''USB control transfer in. + + Request for a control message from the device. + ''' + return self._connection.controlTransfer( + self.FTDI_DEVICE_IN_REQTYPE, + request, + 0, + self._index, + buf, + length, + self.USB_READ_TIMEOUT_MILLIS + ) + + def _set_break(self, break_): + '''Start or stop a break exception event on the serial line. + + Parameters: + break_ (bool): either start or stop break event. + ''' + if break_: + value = self._lineprop | (0x01 << 14) + if self._ctrl_transfer_out(self.SIO_SET_DATA, value, self._index): + raise SerialException('Unable to start break sequence') + else: + value = self._lineprop & ~(0x01 << 14) + if self._ctrl_transfer_out(self.SIO_SET_DATA, value, self._index): + raise SerialException('Unable to stop break sequence') + self._lineprop = value + + def _set_dtr(self, state): + '''Set dtr line. + + Parameters: + state (bool): new DTR logical level. + ''' + value = state and self.SIO_SET_DTR_HIGH or self.SIO_SET_DTR_LOW + if self._ctrl_transfer_out(self.SIO_SET_MODEM_CTRL, value, self._index): + raise SerialException('Unable to set DTR line') + + def _set_rts(self, state): + '''Set rts line. + + Parameters: + state (bool): new RTS logical level. + ''' + value = state and self.SIO_SET_RTS_HIGH or self.SIO_SET_RTS_LOW + if self._ctrl_transfer_out(self.SIO_SET_MODEM_CTRL, value, self._index): + raise SerialException('Unable to set RTS line') + + def _set_dtr_rts(self, dtr, rts): + '''Set dtr and rts lines at once. + + Parameters: + dtr (bool): new DTR logical level. + rts (bool): new RTS logical level. + ''' + value = 0 + value |= dtr and self.SIO_SET_DTR_HIGH or self.SIO_SET_DTR_LOW + value |= rts and self.SIO_SET_RTS_HIGH or self.SIO_SET_RTS_LOW + if self._ctrl_transfer_out(self.SIO_SET_FLOW_CTRL, value, self._index): + raise SerialException('Unable to set DTR/RTS lines') + + def _purgeHwBuffers(self, purgeReadBuffers, purgeWriteBuffers): '''Set serial port parameters. Parameters: @@ -247,19 +468,22 @@ class FtdiSerial(SerialBase): result = self._ctrl_transfer_out( self.SIO_RESET, self.SIO_RESET_PURGE_RX, - 0) + self._index + ) if result != 0: raise SerialException( - "Flushing RX failed: result={}".format(result)) + "Flushing RX failed: result={}".format(result) + ) if purgeWriteBuffers: result = self._ctrl_transfer_out( self.SIO_RESET, self.SIO_RESET_PURGE_TX, - 0) + self._index) if result != 0: raise SerialException( - "Flushing TX failed: result={}".format(result)) + "Flushing TX failed: result={}".format(result) + ) return True @@ -280,11 +504,14 @@ class FtdiSerial(SerialBase): self._read_endpoint, buf, self.DEFAULT_READ_BUFFER_SIZE, - self.USB_READ_TIMEOUT_MILLIS) + self.USB_READ_TIMEOUT_MILLIS + ) if totalBytesRead < self.MODEM_STATUS_HEADER_LENGTH: raise SerialException( "Expected at least {} bytes".format( - self.MODEM_STATUS_HEADER_LENGTH)) + self.MODEM_STATUS_HEADER_LENGTH + ) + ) # Translate raw data into serial port data. read = bytearray() @@ -292,7 +519,8 @@ class FtdiSerial(SerialBase): buf, read, totalBytesRead, - self._read_endpoint.getMaxPacketSize()) + self._read_endpoint.getMaxPacketSize() + ) return bytes(read) @@ -315,13 +543,59 @@ class FtdiSerial(SerialBase): actual, value, index = self._convert_baudrate(baudrate) delta = 100*abs(float(actual-baudrate))/baudrate if delta > self.BAUDRATE_TOLERANCE: - raise ValueError('Baudrate tolerance exceeded: %.02f%% ' - '(wanted %d, achievable %d)' % - (delta, baudrate, actual)) + raise ValueError( + 'Baudrate tolerance exceeded: %.02f%% ' + '(wanted %d, achievable %d)' % + (delta, baudrate, actual) + ) result = self._ctrl_transfer_out(self.SIO_SET_BAUDRATE, value, index) if result != 0: - raise SerialException('Unable to set baudrate') + raise SerialException('Unable to set baudrate.') + def _set_line_property(self, bits, stopbits, parity, break_=0): + '''Set serial port line property. + + Parameters: + bits (int): number of bits in data(5, 6, 7 or 8). + stopbits (float): number of stop bits(1, 1.5, 2). + parity (str): 'N', 'E', 'O', 'M' or 'S'. + break_ (int): 0 for break off or 1 for break on. + ''' + value = bits & 0x0F + + if parity == 'N': + value |= (0x00 << 8) + elif parity == 'O': + value |= (0x01 << 8) + elif parity == 'E': + value |= (0x02 << 8) + elif parity == 'M': + value |= (0x03 << 8) + elif parity == 'S': + value |= (0x04 << 8) + else: + raise ValueError('Unknown parity value: {}'.format(parity)) + + if stopbits == 1: + value |= (0x00 << 11) + elif stopbits == 1.5: + value |= (0x01 << 11) + elif stopbits == 2: + value |= (0x02 << 11) + else: + raise ValueError("Unknown stopbits value: {}".format(stopbits)) + + if break_ == self.BREAK_ON: + value |= 0x01 << 14 + + result = self._ctrl_transfer_out(self.SIO_SET_DATA, value, self._index) + if result != 0: + raise SerialException( + 'Setting line property failed: result={}'.format(result) + ) + + self._lineprop = value + def _setParameters(self, baudrate, databits, parity, stopbits): '''Set serial port parameters. @@ -333,57 +607,54 @@ class FtdiSerial(SerialBase): ''' self._set_baudrate(baudrate) + self._set_line_property(databits, stopbits, parity) - config = databits + def _poll_modem_status(self): + '''Poll modem status information. - if parity == 'N': - config |= (0x00 << 8) - elif parity == 'O': - config |= (0x01 << 8) - elif parity == 'E': - config |= (0x02 << 8) - elif parity == 'M': - config |= (0x03 << 8) - elif parity == 'S': - config |= (0x04 << 8) - else: - raise ValueError("Unknown parity value: {}".format(parity)) + This function allows the retrieve the two status bytes of the + device, useful in UART mode. + FTDI device does not have a so-called USB "interrupt" end-point, + event polling on the UART interface is done through the regular + control endpoint. + Returns: + status (int): modem status, as a proprietary bitfield + ''' + buf = bytearray(self.DEFAULT_READ_BUFFER_SIZE) + result = self._ctrl_transfer_in(self.SIO_POLL_MODEM_STATUS, buf, 2) + if result != 2: + raise SerialException('Unable to get modem status.') + buf = buf[:2] + status, = unpack('> 3) | \ - (frac_code[best_divisor & 7] << 14) + encoded_divisor = (best_divisor >> 3) \ + | (frac_code[best_divisor & 7] << 14) # Deal with special cases for encoded value if encoded_divisor == 1: encoded_divisor = 0 # 3000000 baud @@ -531,21 +802,23 @@ class FtdiSerial(SerialBase): Returns: result (int): the number of payload bytes. ''' - packetsCount = totalBytesRead // maxPacketSize + ( - 0 if totalBytesRead % maxPacketSize == 0 else 1) + packetsCount = totalBytesRead // maxPacketSize + \ + (0 if totalBytesRead % maxPacketSize == 0 else 1) for packetIdx in range(packetsCount): - count = ((totalBytesRead % maxPacketSize) - - self.MODEM_STATUS_HEADER_LENGTH) if ( - packetIdx == (packetsCount - 1)) else ( - maxPacketSize - self.MODEM_STATUS_HEADER_LENGTH) + count = ( + (totalBytesRead % maxPacketSize) \ + - self.MODEM_STATUS_HEADER_LENGTH + ) if (packetIdx == (packetsCount - 1)) else \ + (maxPacketSize - self.MODEM_STATUS_HEADER_LENGTH) if count > 0: usb.arraycopy( src, packetIdx * maxPacketSize + self.MODEM_STATUS_HEADER_LENGTH, dest, - packetIdx * ( - maxPacketSize - self.MODEM_STATUS_HEADER_LENGTH), - count) + packetIdx \ + * (maxPacketSize - self.MODEM_STATUS_HEADER_LENGTH), + count + ) return totalBytesRead - (packetsCount * 2) \ No newline at end of file