diff --git a/example/example.py b/example/example.py new file mode 100644 index 0000000..57da187 --- /dev/null +++ b/example/example.py @@ -0,0 +1,14 @@ +# When this script is run for the first time, it might prompty you for permission. +# Accept the permission and run this script again, then it should send the data as expected. + +# Kivy is needed for pyjnius behind the scene. +import kivy +from usb4a import usb +from usbserial4a.serial4a import get_serial_port + +usb_device_list = usb.get_usb_device_list() +if usb_device_list: + serial_port = get_serial_port(usb_device_list[0].getDeviceName(), 9600, 8, 'N', 1) + if serial_port and serial_port.is_open: + serial_port.write(b'Hello world!') + serial_port.close() diff --git a/intent-filter.xml b/intent-filter.xml new file mode 100644 index 0000000..6dd9e32 --- /dev/null +++ b/intent-filter.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/usbserial4a/__init__.py b/usbserial4a/__init__.py new file mode 100644 index 0000000..c123acd --- /dev/null +++ b/usbserial4a/__init__.py @@ -0,0 +1,12 @@ +'''Python package for Android USB host serial port. + +The serial port class extends pyserial's SerialBase class. +So it can be used in the same manner as serial.Serial from pyserial. + +Author: Quan Lin +License: MIT +Requires: kivy, pyjnius, pyserial, usb4a +''' + +# Project version +__version__ = '0.1.0' diff --git a/usbserial4a/ftdiserial4a.py b/usbserial4a/ftdiserial4a.py new file mode 100644 index 0000000..223817f --- /dev/null +++ b/usbserial4a/ftdiserial4a.py @@ -0,0 +1,401 @@ +from serial.serialutil import SerialBase, SerialException +from usb4a import usb + +class FtdiSerial(SerialBase): + '''FTDI serial port.''' + + # Requests + SIO_RESET = 0 # Reset the port + SIO_SET_MODEM_CTRL = 1 # Set the modem control register + SIO_SET_FLOW_CTRL = 2 # Set flow control register + SIO_SET_BAUDRATE = 3 # Set baud rate + SIO_SET_DATA = 4 # Set the data characteristics of the port + SIO_POLL_MODEM_STATUS = 5 # Get line status + SIO_SET_EVENT_CHAR = 6 # Change event character + SIO_SET_ERROR_CHAR = 7 # Change error character + SIO_SET_LATENCY_TIMER = 9 # Change latency timer + SIO_GET_LATENCY_TIMER = 10 # Get latency timer + SIO_SET_BITMODE = 11 # Change bit mode + SIO_READ_PINS = 12 # Read GPIO pin value + + # Eeprom requests + SIO_EEPROM = 0x90 + SIO_READ_EEPROM = SIO_EEPROM + 0 # Read EEPROM content + SIO_WRITE_EEPROM = SIO_EEPROM + 1 # Write EEPROM content + SIO_ERASE_EEPROM = SIO_EEPROM + 2 # Erase EEPROM content + + # Reset commands + SIO_RESET_SIO = 0 # Reset device + SIO_RESET_PURGE_RX = 1 # Drain RX buffer + SIO_RESET_PURGE_TX = 2 # Drain TX buffer + + # Flow control + SIO_DISABLE_FLOW_CTRL = 0x0 + SIO_RTS_CTS_HS = (0x1 << 8) + SIO_DTR_DSR_HS = (0x2 << 8) + SIO_XON_XOFF_HS = (0x4 << 8) + SIO_SET_DTR_MASK = 0x1 + SIO_SET_DTR_HIGH = (SIO_SET_DTR_MASK | (SIO_SET_DTR_MASK << 8)) + SIO_SET_DTR_LOW = (0x0 | (SIO_SET_DTR_MASK << 8)) + SIO_SET_RTS_MASK = 0x2 + SIO_SET_RTS_HIGH = (SIO_SET_RTS_MASK | (SIO_SET_RTS_MASK << 8)) + SIO_SET_RTS_LOW = (0x0 | (SIO_SET_RTS_MASK << 8)) + + # Clocks and baudrates + BUS_CLOCK_BASE = 6.0E6 # 6 MHz + BUS_CLOCK_HIGH = 30.0E6 # 30 MHz + BAUDRATE_REF_BASE = int(3.0E6) # 3 MHz + BAUDRATE_REF_HIGH = int(12.0E6) # 12 MHz + BAUDRATE_REF_SPECIAL = int(2.0E6) # 3 MHz + BAUDRATE_TOLERANCE = 3.0 # acceptable clock drift, in % + BITBANG_CLOCK_MULTIPLIER = 4 + + DEFAULT_READ_BUFFER_SIZE = 16 * 1024 + DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024 + + FTDI_DEVICE_OUT_REQTYPE = usb.build_usb_control_request_type(usb.UsbConstants.USB_DIR_OUT, usb.UsbConstants.USB_TYPE_VENDOR, 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_WRITE_TIMEOUT_MILLIS = 5000 + USB_READ_TIMEOUT_MILLIS = 5000 + + # Length of the modem status header, transmitted with every read. + MODEM_STATUS_HEADER_LENGTH = 2 + + def __init__(self, *args, **kwargs): + self._device = None + self._connection = None + self._bcd_device = None + self._interface = None + self._index = None + self._control_endpoint = None + self._read_endpoint = None + self._write_endpoint = None + super(FtdiSerial, self).__init__(*args, **kwargs) + + def open(self): + self.close() + + device = usb.get_usb_device(self.portstr) + if not device: + raise SerialException("Device not present {}".format(self.portstr)) + + if not usb.has_usb_permission(device): + usb.request_usb_permission(device) + return + + connection = usb.get_usb_manager().openDevice(device) + if not connection: + raise SerialException("Failed to open device!") + + self._device = device + self._connection = connection + + raw_descriptors = self._connection.getRawDescriptors() + self._bcd_device = raw_descriptors[12] + raw_descriptors[13] * 256 + + for i in range(self._device.getInterfaceCount()): + if i == 0: + self._interface = self._device.getInterface(i) + if not self._connection.claimInterface(self._device.getInterface(i), 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)): + self._control_endpoint = ep + 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)): + self._write_endpoint = ep + + #: Check that all endpoints are good + if None in [self._write_endpoint, self._read_endpoint]: + raise SerialException("Could not establish all endpoints!") + + self.is_open = True + self._reconfigure_port() + + def close(self): + if self._connection: + self._connection.close() + self._connection = None + self.is_open = False + + def reset(self): + if self._connection: + result = self._ctrl_transfer_out(self.SIO_RESET, self.SIO_RESET_SIO, 0) + if result != 0: + raise SerialException("Reset failed: result={}".format(result)) + + def read(self, data_length): + if not self.is_open: + return None + if not self._read_endpoint: + raise SerialException("Read endpoint does not exist!") + + buf = bytearray(data_length) + timeout = int(self._timeout * 1000 if self._timeout else self.USB_READ_TIMEOUT_MILLIS) + totalBytesRead = self._connection.bulkTransfer(self._read_endpoint, buf, data_length, timeout) + if totalBytesRead < self.MODEM_STATUS_HEADER_LENGTH: + raise SerialException("Expected at least {} bytes".format(self.MODEM_STATUS_HEADER_LENGTH)) + + dest = bytearray() + self._filterStatusBytes(buf, dest, totalBytesRead, self._read_endpoint.getMaxPacketSize()) + return dest + + def write(self, data): + if not self.is_open: + return None + offset = 0 + timeout = int(self._write_timeout * 1000 if self._write_timeout else self.USB_WRITE_TIMEOUT_MILLIS) + wrote = 0 + while offset < len(data): + data_length = min(len(data) - offset, self.DEFAULT_WRITE_BUFFER_SIZE) + buf = data[offset:offset + data_length] + i = self._connection.bulkTransfer(self._write_endpoint, + buf, + data_length, + timeout) + if i <= 0: + raise SerialException("Failed to write {}: {}".format(buf, i)) + offset += data_length + wrote += i + return wrote + + def set_baudrate(self, baudrate): + '''Change the current UART baudrate. + + The FTDI device is not able to use an arbitrary baudrate. Its + internal dividors are only able to achieve some baudrates. + It attemps to find the closest configurable baudrate and if + the deviation from the requested baudrate is too high, it rejects + the configuration. + see :py:attr:`baudrate` for the exact selected baudrate. + :py:const:`BAUDRATE_TOLERANCE` defines the maximum deviation, which + matches standard UART clock drift (3%) + :param int baudrate: the new baudrate for the UART. + :raise ValueError: if deviation from selected baudrate is too large + :rause SerialException: on IO Error + ''' + 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)) + result = self._ctrl_transfer_out(self.SIO_SET_BAUDRATE, value, index) + if result != 0: + raise SerialException('Unable to set baudrate') + # self.baudrate = baudrate + + def setParameters(self, baudrate, databits, stopbits, parity): + self.set_baudrate(baudrate) + + config = databits + + 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)) + + if stopbits == 1: + config |= (0x00 << 11) + elif stopbits == 1.5: + config |= (0x01 << 11) + elif stopbits == 2: + config |= (0x02 << 11) + else: + raise ValueError("Unknown stopbits value: {}".format(stopbits)) + + result = self._ctrl_transfer_out(self.SIO_SET_DATA, config, 0) + if result != 0: + raise SerialException("Setting parameters failed: result={}".format(result)) + + def purgeHwBuffers(self, purgeReadBuffers, purgeWriteBuffers): + if purgeReadBuffers: + result = self._ctrl_transfer_out(self.SIO_RESET, self.SIO_RESET_PURGE_RX, 0) + if result != 0: + raise SerialException("Flushing RX failed: result={}".format(result)) + + if purgeWriteBuffers: + result = self._ctrl_transfer_out(self.SIO_RESET, self.SIO_RESET_PURGE_TX, 0) + if result != 0: + raise SerialException("Flushing TX failed: result={}".format(result)) + + return True + + def _reconfigure_port(self): + self.setParameters(self.baudrate, self.bytesize , self.stopbits, self.parity) + + def _ctrl_transfer_out(self, request, value, index): + return self._connection.controlTransfer(self.FTDI_DEVICE_OUT_REQTYPE, request, value, index, None, 0, self.USB_WRITE_TIMEOUT_MILLIS) + + def _has_mpsse(self): + '''Tell whether the device supports MPSSE (I2C, SPI, JTAG, ...) + + :return: True if the FTDI device supports MPSSE + :rtype: bool + :raise SerialException: if no FTDI port is open + ''' + if not self._bcd_device: + raise SerialException('Device characteristics not yet known!') + return self._bcd_device in (0x0500, 0x0700, 0x0800, 0x0900) + + def _is_legacy(self): + '''Tell whether the device is a low-end FTDI + + :return: True if the FTDI device can only be used as a slow USB-UART + bridge + :rtype: bool + :raise SerialException: if no FTDI port is open + ''' + if not self._bcd_device: + raise SerialException('Device characteristics not yet known!') + return self._bcd_device <= 0x0200 + + def _is_H_series(self): + '''Tell whether the device is a high-end FTDI + + :return: True if the FTDI device is a high-end USB-UART bridge + :rtype: bool + :raise SerialException: if no FTDI port is open + ''' + if not self._bcd_device: + raise SerialException('Device characteristics not yet known!') + return self._bcd_device in (0x0700, 0x0800, 0x0900) + + def _convert_baudrate(self, baudrate): + '''Convert a requested baudrate into the closest possible one. + + Convert a requested baudrate into the closest possible baudrate that + can be assigned to the FTDI device. + ''' + if baudrate < ((2*self.BAUDRATE_REF_BASE)//(2*16384+1)): + raise ValueError('Invalid baudrate (too low)') + if baudrate > self.BAUDRATE_REF_BASE: + if not self._is_H_series or \ + baudrate > self.BAUDRATE_REF_HIGH: + raise ValueError('Invalid baudrate (too high)') + refclock = self.BAUDRATE_REF_HIGH + hispeed = True + else: + refclock = self.BAUDRATE_REF_BASE + hispeed = False + # AM legacy device only supports 3 sub-integer dividers, where the + # other devices supports 8 sub-integer dividers + am_adjust_up = [0, 0, 0, 1, 0, 3, 2, 1] + am_adjust_dn = [0, 0, 0, 1, 0, 1, 2, 3] + # Sub-divider code are not ordered in the natural order + frac_code = [0, 3, 2, 4, 1, 5, 6, 7] + divisor = (refclock*8) // baudrate + if self._is_legacy: + # Round down to supported fraction (AM only) + divisor -= am_adjust_dn[divisor & 7] + # Try this divisor and the one above it (because division rounds down) + best_divisor = 0 + best_baud = 0 + best_baud_diff = 0 + for i in range(2): + try_divisor = divisor + i + if not hispeed: + # Round up to supported divisor value + if try_divisor <= 8: + # Round up to minimum supported divisor + try_divisor = 8 + elif self._is_legacy and \ + try_divisor < 12: + # BM doesn't support divisors 9 through 11 inclusive + try_divisor = 12 + elif divisor < 16: + # AM doesn't support divisors 9 through 15 inclusive + try_divisor = 16 + else: + if self._is_legacy: + # Round up to supported fraction (AM only) + try_divisor += am_adjust_up[try_divisor & 7] + if try_divisor > 0x1FFF8: + # Round down to maximum supported div value (AM) + try_divisor = 0x1FFF8 + else: + if try_divisor > 0x1FFFF: + # Round down to maximum supported div value (BM) + try_divisor = 0x1FFFF + # Get estimated baud rate (to nearest integer) + baud_estimate = ((refclock*8) + (try_divisor//2))//try_divisor + # Get absolute difference from requested baud rate + if baud_estimate < baudrate: + baud_diff = baudrate - baud_estimate + else: + baud_diff = baud_estimate - baudrate + if (i == 0) or (baud_diff < best_baud_diff): + # Closest to requested baud rate so far + best_divisor = try_divisor + best_baud = baud_estimate + best_baud_diff = baud_diff + if baud_diff == 0: + break + # Encode the best divisor value + 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 + elif encoded_divisor == 0x4001: + encoded_divisor = 1 # 2000000 baud (BM only) + # Split into "value" and "index" values + value = encoded_divisor & 0xFFFF + if self._has_mpsse: + index = (encoded_divisor >> 8) & 0xFFFF + index &= 0xFF00 + index |= self._index + else: + index = (encoded_divisor >> 16) & 0xFFFF + if hispeed: + index |= 1 << 9 # use hispeed mode + return (best_baud, value, index) + + def _filterStatusBytes(self, src, dest, totalBytesRead, maxPacketSize): + '''Filter FTDI status bytes from buffer + + @param bytearray src The source buffer (which contains status bytes) + @param bytearray dest The destination buffer to write the status bytes into (can be src) + @param int totalBytesRead Number of bytes read to src + @param int maxPacketSize The USB endpoint max packet size + @return int The number of payload bytes + ''' + 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 + if count > 0: + usb.arraycopy(src, + packetIdx * maxPacketSize + self.MODEM_STATUS_HEADER_LENGTH, + dest, + packetIdx * (maxPacketSize - self.MODEM_STATUS_HEADER_LENGTH), + count + ) + + return totalBytesRead - (packetsCount * 2) + + + + + + + + + + \ No newline at end of file diff --git a/usbserial4a/serial4a.py b/usbserial4a/serial4a.py new file mode 100644 index 0000000..e39f5ea --- /dev/null +++ b/usbserial4a/serial4a.py @@ -0,0 +1,46 @@ +'''Android USB host serial port. + +Functions: +get_serial_port +''' + +from usb4a import usb +from .ftdiserial4a import FtdiSerial + +FTDI_VENDOR_ID = 0x0403 +VENDOR_IDS = {'ftdi': FTDI_VENDOR_ID} +PRODUCT_IDS = { + FTDI_VENDOR_ID: { + 'ft232': 0x6001, + 'ft232r': 0x6001, + 'ft232h': 0x6014, + 'ft2232': 0x6010, + 'ft2232d': 0x6010, + 'ft2232h': 0x6010, + 'ft4232': 0x6011, + 'ft4232h': 0x6011, + 'ft230x': 0x6015 + } + } + +def get_serial_port(device_name, *args, **kwargs): + '''Get a USB serial port from the system. + + The parameters are compatible with serial.Serial from pyserial. + The class of the returned object extends SerialBase from pyserial. + + Parameters: + device_name (str): the name of the USB device. + + Returns: + USB serial port: an object representing the USB serial port. + ''' + device = usb.get_usb_device(device_name) + if device: + if device.getVendorId() == VENDOR_IDS['ftdi']: + return FtdiSerial(device_name, *args, **kwargs) + else: + raise usb.USBError('Vendor ID is not supported!') + else: + raise usb.USBError('Device does not exist!') + \ No newline at end of file diff --git a/xml/device_filter.xml b/xml/device_filter.xml new file mode 100644 index 0000000..65b76dc --- /dev/null +++ b/xml/device_filter.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file