From adb22f718e03ee36a8977b4a517d05d74d748795 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Fri, 9 Mar 2018 22:37:06 +0100 Subject: [PATCH 1/3] build tools update; instrumented device test --- .idea/misc.xml | 37 +- .idea/runConfigurations.xml | 12 + build.gradle | 8 +- gradle/wrapper/gradle-wrapper.jar | Bin 49896 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 12 +- .../arduino_leonardo_bridge.ino | 64 ++ test/rfc2217_server.diff | 42 + {arduino => test/serial_test}/serial_test.ino | 0 usbSerialExamples/build.gradle | 9 +- usbSerialForAndroid/build.gradle | 17 +- .../src/androidTest/AndroidManifest.xml | 7 + .../hoho/android/usbserial/DeviceTest.java | 855 ++++++++++++++++++ 12 files changed, 1032 insertions(+), 31 deletions(-) create mode 100644 .idea/runConfigurations.xml create mode 100644 test/arduino_leonardo_bridge/arduino_leonardo_bridge.ino create mode 100644 test/rfc2217_server.diff rename {arduino => test/serial_test}/serial_test.ino (100%) create mode 100644 usbSerialForAndroid/src/androidTest/AndroidManifest.xml create mode 100644 usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java diff --git a/.idea/misc.xml b/.idea/misc.xml index aaaf922..13c4629 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,19 +1,30 @@ - - + + + - - - - - - - - - - - + diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0c42278..17033eb 100644 --- a/build.gradle +++ b/build.gradle @@ -2,15 +2,17 @@ buildscript { repositories { - mavenCentral() + jcenter() + google() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'com.android.tools.build:gradle:3.0.1' } } allprojects { repositories { - mavenCentral() + jcenter() + google() } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8c0fb64a8698b08ecc4158d828ca593c4928e9dd..13372aef5e24af05341d49695ee84e5f9b594659 100644 GIT binary patch delta 46904 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�{>`y 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f3d36d0..8f072ca 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jun 23 00:11:28 EDT 2015 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +#Thu Oct 26 20:06:55 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/test/arduino_leonardo_bridge/arduino_leonardo_bridge.ino b/test/arduino_leonardo_bridge/arduino_leonardo_bridge.ino new file mode 100644 index 0000000..e7071d9 --- /dev/null +++ b/test/arduino_leonardo_bridge/arduino_leonardo_bridge.ino @@ -0,0 +1,64 @@ +/* + bridge USB-serial to hardware-serial + + for Arduinos based on ATmega32u4 (Leonardo and compatible Pro Micro, Micro) + hardware serial is configured with baud-rate, databits, stopbits, parity as send over USB + + see https://github.com/arduino/Arduino/tree/master/hardware/arduino/avr/cores/arduino + -> CDC.cpp|HardwareSerial.cpp for serial implementation details + + this sketch is mainly for demonstration / test of CDC communication + performance as real usb-serial bridge would be inacceptable as each byte is send in separate USB packet +*/ + +uint32_t baud = 9600; +uint8_t databits = 8; +uint8_t stopbits = 1; +uint8_t parity = 0; + +void setup() { + Serial.begin(baud); // USB + Serial1.begin(baud, SERIAL_8N1); +} + +void loop() { + // show USB connected state + if (Serial) TXLED1; + else TXLED0; + + // configure hardware serial + if (Serial.baud() != baud || + Serial.numbits() != databits || + Serial.stopbits() != stopbits || + Serial.paritytype() != parity) { + baud = Serial.baud(); + databits = Serial.numbits(); + stopbits = Serial.stopbits(); + parity = Serial.paritytype(); + uint8_t config = 0; // ucsrc register + switch (databits) { + case 5: break; + case 6: config |= 2; break; + case 7: config |= 4; break; + case 8: config |= 6; break; + default: config |= 6; + } + switch (stopbits) { + case 2: config |= 8; + // 1.5 stopbits not supported + } + switch (parity) { + case 1: config |= 0x30; break; // odd + case 2: config |= 0x20; break; // even + // mark, space not supported + } + Serial1.end(); + Serial1.begin(baud, config); + } + + // bridge + if (Serial.available() > 0) + Serial1.write(Serial.read()); + if (Serial1.available() > 0) + Serial.write(Serial1.read()); +} diff --git a/test/rfc2217_server.diff b/test/rfc2217_server.diff new file mode 100644 index 0000000..a5d09a2 --- /dev/null +++ b/test/rfc2217_server.diff @@ -0,0 +1,42 @@ +*** /n/archiv/python/rfc2217_server.py 2018-03-10 09:02:07.613771600 +0100 +--- rfc2217_server.py 2018-03-09 20:57:44.933717100 +0100 +*************** +*** 26,31 **** +--- 26,32 ---- + self, + logger=logging.getLogger('rfc2217.server') if debug else None) + self.log = logging.getLogger('redirector') ++ self.dlog = logging.getLogger('data') + + def statusline_poller(self): + self.log.debug('status line poll thread started') +*************** +*** 55,60 **** +--- 56,62 ---- + try: + data = self.serial.read(self.serial.in_waiting or 1) + if data: ++ self.dlog.debug("serial read: "+data.encode('hex')) + # escape outgoing data when needed (Telnet IAC (0xff) character) + self.write(b''.join(self.rfc2217.escape(data))) + except socket.error as msg: +*************** +*** 76,81 **** +--- 78,84 ---- + data = self.socket.recv(1024) + if not data: + break ++ self.dlog.debug("socket read: "+data.encode('hex')) + self.serial.write(b''.join(self.rfc2217.filter(data))) + except socket.error as msg: + self.log.error('{}'.format(msg)) +*************** +*** 132,137 **** +--- 135,141 ---- + logging.basicConfig(level=logging.INFO) + #~ logging.getLogger('root').setLevel(logging.INFO) + logging.getLogger('rfc2217').setLevel(level) ++ logging.getLogger('data').setLevel(level) + + # connect to serial port + ser = serial.serial_for_url(args.SERIALPORT, do_not_open=True) diff --git a/arduino/serial_test.ino b/test/serial_test/serial_test.ino similarity index 100% rename from arduino/serial_test.ino rename to test/serial_test/serial_test.ino diff --git a/usbSerialExamples/build.gradle b/usbSerialExamples/build.gradle index 2da9b1c..cb2be2a 100644 --- a/usbSerialExamples/build.gradle +++ b/usbSerialExamples/build.gradle @@ -1,14 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 22 - buildToolsVersion "22.0.1" + compileSdkVersion 26 + buildToolsVersion "26.0.2" defaultConfig { minSdkVersion 14 - targetSdkVersion 22 + targetSdkVersion 26 - testApplicationId "com.hoho.android.usbserial.examples" testInstrumentationRunner "android.test.InstrumentationTestRunner" } @@ -20,5 +19,5 @@ android { } dependencies { - compile project(':usbSerialForAndroid') + implementation project(':usbSerialForAndroid') } diff --git a/usbSerialForAndroid/build.gradle b/usbSerialForAndroid/build.gradle index c3d835c..cd13792 100644 --- a/usbSerialForAndroid/build.gradle +++ b/usbSerialForAndroid/build.gradle @@ -3,12 +3,13 @@ apply plugin: 'maven' apply plugin: 'signing' android { - compileSdkVersion 19 - buildToolsVersion "19.1" + compileSdkVersion 26 + buildToolsVersion "26.0.2" defaultConfig { - minSdkVersion 12 - targetSdkVersion 19 + minSdkVersion 14 + targetSdkVersion 26 + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -19,6 +20,14 @@ android { } } +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support:support-annotations:27.1.0' + androidTestImplementation 'com.android.support.test:runner:1.0.1' + androidTestImplementation 'commons-net:commons-net:3.6' + androidTestImplementation 'org.apache.commons:commons-lang3:3.7' +} + group = "com.hoho.android" version = "0.2.0-SNAPSHOT" diff --git a/usbSerialForAndroid/src/androidTest/AndroidManifest.xml b/usbSerialForAndroid/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..51ad082 --- /dev/null +++ b/usbSerialForAndroid/src/androidTest/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java new file mode 100644 index 0000000..b0c48af --- /dev/null +++ b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java @@ -0,0 +1,855 @@ +/* + * test setup + * - android device with ADB over Wi-Fi + * - to set up ADB over Wi-Fi with custom roms you typically can do it from: Android settings -> Developer options + * - for other devices you first have to manually connect over USB and enable Wi-Fi as shown here: + * https://developer.android.com/studio/command-line/adb.html + * - windows/linux machine running rfc2217_server.py + * python + pyserial + https://github.com/pyserial/pyserial/blob/master/examples/rfc2217_server.py + * for developing this test it was essential to see all data (see test/rfc2217_server.diff, run python script with '-v -v' option) + * - all suppported usb <-> serial converter + * as CDC test device use an arduino leonardo / pro mini programmed with arduino_leonardo_bridge.ino + * + * restrictions + * - as real hardware is used, timing might need tuning. see: + * - Thread.sleep(...) + * - obj.wait(...) + * - some tests fail sporadically. typical workarounds are: + * - reconnect device + * - run test individually + * - increase sleep? + * - missing functionality on certain devices, see: + * - if(rfc2217_server_nonstandard_baudrates) + * - if(usbSerialDriver instanceof ...) + * + */ +package com.hoho.android.usbserial; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbManager; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; + +import com.hoho.android.usbserial.driver.CdcAcmSerialDriver; +import com.hoho.android.usbserial.driver.Ch34xSerialDriver; +import com.hoho.android.usbserial.driver.Cp21xxSerialDriver; +import com.hoho.android.usbserial.driver.FtdiSerialDriver; +import com.hoho.android.usbserial.driver.ProbeTable; +import com.hoho.android.usbserial.driver.ProlificSerialDriver; +import com.hoho.android.usbserial.driver.UsbSerialDriver; +import com.hoho.android.usbserial.driver.UsbSerialPort; +import com.hoho.android.usbserial.driver.UsbSerialProber; +import com.hoho.android.usbserial.util.SerialInputOutputManager; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.net.telnet.InvalidTelnetOptionException; +import org.apache.commons.net.telnet.TelnetClient; +import org.apache.commons.net.telnet.TelnetCommand; +import org.apache.commons.net.telnet.TelnetOptionHandler; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(AndroidJUnit4.class) +public class DeviceTest implements SerialInputOutputManager.Listener { + + private final static String rfc2217_server_host = "192.168.0.171"; + private final static int rfc2217_server_port = 2217; + private final static boolean rfc2217_server_nonstandard_baudrates = false; // false on Windows + + private final static int TELNET_READ_WAIT = 500; + private final static int USB_READ_WAIT = 500; + private final static int USB_WRITE_WAIT = 500; + + private final static String TAG = "DeviceTest"; + private final static byte RFC2217_COM_PORT_OPTION = 0x2c; + private final static byte RFC2217_SET_BAUDRATE = 1; + private final static byte RFC2217_SET_DATASIZE = 2; + private final static byte RFC2217_SET_PARITY = 3; + private final static byte RFC2217_SET_STOPSIZE = 4; + + private Context context; + private UsbSerialDriver usbSerialDriver; + private UsbDeviceConnection usbDeviceConnection; + private UsbSerialPort usbSerialPort; + private SerialInputOutputManager usbIoManager; + private final Deque usbReadBuffer = new LinkedList<>(); + private boolean usbReadBlock = false; + + private static TelnetClient telnetClient; + private static InputStream telnetReadStream; + private static OutputStream telnetWriteStream; + private static Integer[] telnetComPortOptionCounter = {0}; + private int telnetWriteDelay = 0; + + @BeforeClass + public static void setUpFixture() throws Exception { + telnetClient = null; + // postpone fixture setup to first test, because exceptions are not reported for @BeforeClass + // and test terminates with missleading 'Empty test suite' + } + + public static void setUpFixtureInt() throws Exception { + if(telnetClient != null) + return; + telnetClient = new TelnetClient(); + telnetClient.addOptionHandler(new TelnetOptionHandler(RFC2217_COM_PORT_OPTION, false, false, false, false) { + @Override + public int[] answerSubnegotiation(int[] suboptionData, int suboptionLength) { + telnetComPortOptionCounter[0] += 1; + return super.answerSubnegotiation(suboptionData, suboptionLength); + } + }); + + telnetClient.setConnectTimeout(2000); + telnetClient.connect(rfc2217_server_host, rfc2217_server_port); + telnetClient.setTcpNoDelay(true); + telnetWriteStream = telnetClient.getOutputStream(); + telnetReadStream = telnetClient.getInputStream(); + } + + @Before + public void setUp() throws Exception { + setUpFixtureInt(); + telnetClient.sendAYT(1000); // not corrctly handled by rfc2217_server.py, but WARNING output "ignoring Telnet command: '\xf6'" is a nice separator between tests + telnetComPortOptionCounter[0] = 0; + telnetWriteDelay = 0; + + context = InstrumentationRegistry.getContext(); + final UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + List availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager); + assertEquals("no usb device found", 1, availableDrivers.size()); + usbSerialDriver = availableDrivers.get(0); + assertEquals(1, usbSerialDriver.getPorts().size()); + usbSerialPort = usbSerialDriver.getPorts().get(0); + Log.i(TAG, "Using USB device "+ usbSerialDriver.getClass().getSimpleName()); + + + if (!usbManager.hasPermission(usbSerialPort.getDriver().getDevice())) { + final Boolean[] granted = {Boolean.FALSE}; + BroadcastReceiver usbReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + granted[0] = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); + synchronized (granted) { + granted.notify(); + } + } + }; + PendingIntent permissionIntent = PendingIntent.getBroadcast(context, 0, new Intent("com.android.example.USB_PERMISSION"), 0); + IntentFilter filter = new IntentFilter("com.android.example.USB_PERMISSION"); + context.registerReceiver(usbReceiver, filter); + usbManager.requestPermission(usbSerialDriver.getDevice(), permissionIntent); + synchronized (granted) { + granted.wait(5000); + } + assertTrue("USB permission dialog not confirmed", granted[0]); + } + usbDeviceConnection = usbManager.openDevice(usbSerialDriver.getDevice()); + usbSerialPort.open(usbDeviceConnection); + usbSerialPort.setDTR(true); + usbSerialPort.setRTS(true); + usbIoManager = new SerialInputOutputManager(usbSerialPort, this); + Executors.newSingleThreadExecutor().submit(usbIoManager); + + synchronized (usbReadBuffer) { + usbReadBuffer.clear(); + } + } + + @After + public void tearDown() throws IOException { + try { + usbRead(0); + } catch (Exception ignored) {} + try { + telnetRead(0); + } catch (Exception ignored) {} + + try { + usbIoManager.setListener(null); + usbIoManager.stop(); + } catch (Exception ignored) {} + try { + usbSerialPort.setDTR(false); + usbSerialPort.setRTS(false); + usbSerialPort.close(); + } catch (Exception ignored) {} + try { + usbDeviceConnection.close(); + } catch (Exception ignored) {} + usbIoManager = null; + usbSerialPort = null; + usbDeviceConnection = null; + usbSerialDriver = null; + } + + @AfterClass + public static void tearDownFixture() throws Exception { + try { + telnetClient.disconnect(); + } catch (Exception ignored) {} + telnetReadStream = null; + telnetWriteStream = null; + telnetClient = null; + } + + // wait full time + private byte[] telnetRead() throws Exception { + return telnetRead(-1); + } + + private byte[] telnetRead(int expectedLength) throws Exception { + long end = System.currentTimeMillis() + TELNET_READ_WAIT; + ByteBuffer buf = ByteBuffer.allocate(4096); + while(System.currentTimeMillis() < end) { + if(telnetReadStream.available() > 0) { + buf.put((byte) telnetReadStream.read()); + } else { + if (expectedLength >= 0 && buf.position() >= expectedLength) + break; + Thread.sleep(1); + } + } + byte[] data = new byte[buf.position()]; + buf.flip(); + buf.get(data); + return data; + } + + private void telnetWrite(byte[] data) throws Exception{ + if(telnetWriteDelay != 0) { + for(byte b : data) { + telnetWriteStream.write(b); + telnetWriteStream.flush(); + Thread.sleep(telnetWriteDelay); + } + } else { + telnetWriteStream.write(data); + telnetWriteStream.flush(); + } + } + + // wait full time + private byte[] usbRead() throws Exception { + return usbRead(-1); + } + + private byte[] usbRead(int expectedLength) throws Exception { + long end = System.currentTimeMillis() + USB_READ_WAIT; + ByteBuffer buf = ByteBuffer.allocate(4096); + if(usbIoManager != null) { + while (System.currentTimeMillis() < end) { + synchronized (usbReadBuffer) { + while(usbReadBuffer.peek() != null) + buf.put(usbReadBuffer.remove()); + } + if (expectedLength >= 0 && buf.position() >= expectedLength) + break; + Thread.sleep(1); + } + + } else { + byte[] b1 = new byte[256]; + while (System.currentTimeMillis() < end) { + int len = usbSerialPort.read(b1, USB_READ_WAIT / 10); + if (len > 0) { + buf.put(b1, 0, len); + } else { + if (expectedLength >= 0 && buf.position() >= expectedLength) + break; + Thread.sleep(1); + } + } + } + byte[] data = new byte[buf.position()]; + buf.flip(); + buf.get(data); + return data; + } + + private void usbWrite(byte[] data) throws IOException { + usbSerialPort.write(data, USB_WRITE_WAIT); + } + + private void usbParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException, InterruptedException { + usbSerialPort.setParameters(baudRate, dataBits, stopBits, parity); + if(usbSerialDriver instanceof CdcAcmSerialDriver) + Thread.sleep(10); // arduino_leonardeo_bridge.ini needs some time + else + Thread.sleep(1); + } + + private void telnetParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException, InterruptedException, InvalidTelnetOptionException { + telnetComPortOptionCounter[0] = 0; + + telnetClient.sendCommand((byte)TelnetCommand.SB); + telnetWriteStream.write(new byte[] {RFC2217_COM_PORT_OPTION, RFC2217_SET_BAUDRATE, (byte)(baudRate>>24), (byte)(baudRate>>16), (byte)(baudRate>>8), (byte)baudRate}); + telnetClient.sendCommand((byte)TelnetCommand.SE); + + telnetClient.sendCommand((byte)TelnetCommand.SB); + telnetWriteStream.write(new byte[] {RFC2217_COM_PORT_OPTION, RFC2217_SET_DATASIZE, (byte)dataBits}); + telnetClient.sendCommand((byte)TelnetCommand.SE); + + telnetClient.sendCommand((byte)TelnetCommand.SB); + telnetWriteStream.write(new byte[] {RFC2217_COM_PORT_OPTION, RFC2217_SET_STOPSIZE, (byte)stopBits}); + telnetClient.sendCommand((byte)TelnetCommand.SE); + + telnetClient.sendCommand((byte)TelnetCommand.SB); + telnetWriteStream.write(new byte[] {RFC2217_COM_PORT_OPTION, RFC2217_SET_PARITY, (byte)(parity+1)}); + telnetClient.sendCommand((byte)TelnetCommand.SE); + + // windows does not like nonstandard baudrates. rfc2217_server.py terminates w/o response + for(int i=0; i<2000; i++) { + if(telnetComPortOptionCounter[0] == 4) break; + Thread.sleep(1); + } + assertEquals("telnet connection lost", 4, telnetComPortOptionCounter[0].intValue()); + } + + @Override + public void onNewData(byte[] data) { + while(usbReadBlock) + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + synchronized (usbReadBuffer) { + usbReadBuffer.add(data); + } + } + + @Override + public void onRunError(Exception e) { + assertTrue("usb connection lost", false); + } + + + @Test + public void baudRate() throws Exception { + byte[] data; + + if (false) { // default baud rate + // CP2102: only works if first connection after attaching device + // PL2303, FTDI: it's not 9600 + telnetParameters(9600, 8, 1, UsbSerialPort.PARITY_NONE); + + telnetWrite("net2usb".getBytes()); + data = usbRead(7); + assertThat(data, equalTo("net2usb".getBytes())); // includes array content in output + //assertArrayEquals("net2usb".getBytes(), data); // only includes array length in output + usbWrite("usb2net".getBytes()); + data = telnetRead(7); + assertThat(data, equalTo("usb2net".getBytes())); + } + + // invalid values + try { + usbParameters(-1, 8, 1, UsbSerialPort.PARITY_NONE); + if (usbSerialDriver instanceof Ch34xSerialDriver) + ; // todo: add range check in driver + else if (usbSerialDriver instanceof FtdiSerialDriver) + ; // todo: add range check in driver + else if (usbSerialDriver instanceof ProlificSerialDriver) + ; // todo: add range check in driver + else if (usbSerialDriver instanceof Cp21xxSerialDriver) + ; // todo: add range check in driver + else if (usbSerialDriver instanceof CdcAcmSerialDriver) + ; // todo: add range check in driver + else + fail("invalid baudrate 0"); + } catch (java.lang.IllegalArgumentException e) { + } + try { + usbParameters(0, 8, 1, UsbSerialPort.PARITY_NONE); + if (usbSerialDriver instanceof ProlificSerialDriver) + ; // todo: add range check in driver + else if (usbSerialDriver instanceof Cp21xxSerialDriver) + ; // todo: add range check in driver + else if (usbSerialDriver instanceof CdcAcmSerialDriver) + ; // todo: add range check in driver + else + fail("invalid baudrate 0"); + } catch (java.lang.ArithmeticException e) { // ch340 + } catch (java.lang.IllegalArgumentException e) { + } + try { + usbParameters(1, 8, 1, UsbSerialPort.PARITY_NONE); + if (usbSerialDriver instanceof FtdiSerialDriver) + ; + else if (usbSerialDriver instanceof ProlificSerialDriver) + ; + else if (usbSerialDriver instanceof Cp21xxSerialDriver) + ; + else if (usbSerialDriver instanceof CdcAcmSerialDriver) + ; + else + fail("invalid baudrate 0"); + } catch (java.io.IOException e) { // ch340 + } catch (java.lang.IllegalArgumentException e) { + } + try { + usbParameters(2<<31, 8, 1, UsbSerialPort.PARITY_NONE); + if (usbSerialDriver instanceof ProlificSerialDriver) + ; + else if (usbSerialDriver instanceof Cp21xxSerialDriver) + ; + else if (usbSerialDriver instanceof CdcAcmSerialDriver) + ; + else + fail("invalid baudrate 2^31"); + } catch (java.lang.ArithmeticException e) { // ch340 + } catch (java.lang.IllegalArgumentException e) { + } + + for(int baudRate : new int[] {2400, 19200, 42000, 115200} ) { + if(baudRate == 42000 && !rfc2217_server_nonstandard_baudrates) + continue; // rfc2217_server.py would terminate + telnetParameters(baudRate, 8, 1, UsbSerialPort.PARITY_NONE); + usbParameters(baudRate, 8, 1, UsbSerialPort.PARITY_NONE); + + telnetWrite("net2usb".getBytes()); + data = usbRead(7); + assertThat(String.valueOf(baudRate)+"/8N1", data, equalTo("net2usb".getBytes())); + usbWrite("usb2net".getBytes()); + data = telnetRead(7); + assertThat(String.valueOf(baudRate)+"/8N1", data, equalTo("usb2net".getBytes())); + } + { // non matching baud rate + telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + usbParameters(2400, 8, 1, UsbSerialPort.PARITY_NONE); + + telnetWrite("net2usb".getBytes()); + data = usbRead(); + assertNotEquals(7, data.length); + usbWrite("usb2net".getBytes()); + data = telnetRead(); + assertNotEquals(7, data.length); + } + } + + @Test + public void dataBits() throws Exception { + byte[] data; + + for(int i: new int[] {0, 4, 9}) { + try { + usbParameters(19200, i, 1, UsbSerialPort.PARITY_NONE); + if (usbSerialDriver instanceof ProlificSerialDriver) + ; // todo: add range check in driver + else if (usbSerialDriver instanceof CdcAcmSerialDriver) + ; // todo: add range check in driver + else + fail("invalid databits "+i); + } catch (java.lang.IllegalArgumentException e) { + } + } + + // telnet -> usb + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(19200, 7, 1, UsbSerialPort.PARITY_NONE); + telnetWrite(new byte[] {0x00}); + Thread.sleep(1); // one bit is 0.05 milliseconds long, wait >> stop bit + telnetWrite(new byte[] {(byte)0xff}); + data = usbRead(2); + assertThat("19200/7N1", data, equalTo(new byte[] {(byte)0x80, (byte)0xff})); + + telnetParameters(19200, 6, 1, UsbSerialPort.PARITY_NONE); + telnetWrite(new byte[] {0x00}); + Thread.sleep(1); + telnetWrite(new byte[] {(byte)0xff}); + data = usbRead(2); + assertThat("19000/6N1", data, equalTo(new byte[] {(byte)0xc0, (byte)0xff})); + + telnetParameters(19200, 5, 1, UsbSerialPort.PARITY_NONE); + telnetWrite(new byte[] {0x00}); + Thread.sleep(1); + telnetWrite(new byte[] {(byte)0xff}); + data = usbRead(2); + assertThat("19000/5N1", data, equalTo(new byte[] {(byte)0xe0, (byte)0xff})); + + // usb -> telnet + telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + usbParameters(19200, 7, 1, UsbSerialPort.PARITY_NONE); + usbWrite(new byte[] {0x00}); + Thread.sleep(1); + usbWrite(new byte[] {(byte)0xff}); + data = telnetRead(2); + assertThat("19000/7N1", data, equalTo(new byte[] {(byte)0x80, (byte)0xff})); + + try { + usbParameters(19200, 6, 1, UsbSerialPort.PARITY_NONE); + usbWrite(new byte[]{0x00}); + Thread.sleep(1); + usbWrite(new byte[]{(byte) 0xff}); + data = telnetRead(2); + assertThat("19000/6N1", data, equalTo(new byte[]{(byte) 0xc0, (byte) 0xff})); + } catch (java.lang.IllegalArgumentException e) { + if (!(usbSerialDriver instanceof FtdiSerialDriver)) + throw e; + } + try { + usbParameters(19200, 5, 1, UsbSerialPort.PARITY_NONE); + usbWrite(new byte[] {0x00}); + Thread.sleep(1); + usbWrite(new byte[] {(byte)0xff}); + data = telnetRead(2); + assertThat("19000/5N1", data, equalTo(new byte[] {(byte)0xe0, (byte)0xff})); + } catch (java.lang.IllegalArgumentException e) { + if (!(usbSerialDriver instanceof FtdiSerialDriver)) + throw e; + } + } + + @Test + public void parity() throws Exception { + byte[] _8n1 = {(byte)0x00, (byte)0x01, (byte)0xfe, (byte)0xff}; + byte[] _7n1 = {(byte)0x00, (byte)0x01, (byte)0x7e, (byte)0x7f}; + byte[] _7o1 = {(byte)0x80, (byte)0x01, (byte)0xfe, (byte)0x7f}; + byte[] _7e1 = {(byte)0x00, (byte)0x81, (byte)0x7e, (byte)0xff}; + byte[] _7m1 = {(byte)0x80, (byte)0x81, (byte)0xfe, (byte)0xff}; + byte[] _7s1 = {(byte)0x00, (byte)0x01, (byte)0x7e, (byte)0x7f}; + byte[] data; + + for(int i: new int[] {-1, 5}) { + try { + usbParameters(19200, 8, 1, i); + fail("invalid parity "+i); + } catch (java.lang.IllegalArgumentException e) { + } + } + + // usb -> telnet + telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + usbWrite(_8n1); + data = telnetRead(4); + assertThat("19200/8N1", data, equalTo(_8n1)); + + usbParameters(19200, 7, 1, UsbSerialPort.PARITY_ODD); + usbWrite(_8n1); + data = telnetRead(4); + assertThat("19200/7O1", data, equalTo(_7o1)); + + usbParameters(19200, 7, 1, UsbSerialPort.PARITY_EVEN); + usbWrite(_8n1); + data = telnetRead(4); + assertThat("19200/7E1", data, equalTo(_7e1)); + + if (usbSerialDriver instanceof CdcAcmSerialDriver) { + // not supported by arduino_leonardo_bridge.ino, other devices might support it + } else { + usbParameters(19200, 7, 1, UsbSerialPort.PARITY_MARK); + usbWrite(_8n1); + data = telnetRead(4); + assertThat("19200/7M1", data, equalTo(_7m1)); + + usbParameters(19200, 7, 1, UsbSerialPort.PARITY_SPACE); + usbWrite(_8n1); + data = telnetRead(4); + assertThat("19200/7S1", data, equalTo(_7s1)); + } + + // telnet -> usb + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetWrite(_8n1); + data = usbRead(4); + assertThat("19200/8N1", data, equalTo(_8n1)); + + telnetParameters(19200, 7, 1, UsbSerialPort.PARITY_ODD); + telnetWrite(_8n1); + data = usbRead(4); + assertThat("19200/7O1", data, equalTo(_7o1)); + + telnetParameters(19200, 7, 1, UsbSerialPort.PARITY_EVEN); + telnetWrite(_8n1); + data = usbRead(4); + assertThat("19200/7E1", data, equalTo(_7e1)); + + if (usbSerialDriver instanceof CdcAcmSerialDriver) { + // not supported by arduino_leonardo_bridge.ino, other devices might support it + } else { + telnetParameters(19200, 7, 1, UsbSerialPort.PARITY_MARK); + telnetWrite(_8n1); + data = usbRead(4); + assertThat("19200/7M1", data, equalTo(_7m1)); + + telnetParameters(19200, 7, 1, UsbSerialPort.PARITY_SPACE); + telnetWrite(_8n1); + data = usbRead(4); + assertThat("19200/7S1", data, equalTo(_7s1)); + + usbParameters(19200, 7, 1, UsbSerialPort.PARITY_ODD); + telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetWrite(_8n1); + data = usbRead(4); + assertThat("19200/8N1", data, equalTo(_7n1)); // read is resilient against errors + } + } + + @Test + public void stopBits() throws Exception { + byte[] data; + + for (int i : new int[]{0, 4}) { + try { + usbParameters(19200, 8, i, UsbSerialPort.PARITY_NONE); + fail("invalid stopbits " + i); + } catch (java.lang.IllegalArgumentException e) { + } + } + + if (usbSerialDriver instanceof CdcAcmSerialDriver) { + // software based bridge in arduino_leonardo_bridge.ino is to slow, other devices might support it + } else { + // shift stopbits into next byte, by using different databits + // a - start bit (0) + // o - stop bit (1) + // d - data bit + + // out 8N2: addddddd doadddddddoo + // 1000001 0 1 + // in 6N1: addddddo adddddo + // 100000 101 + usbParameters(19200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); + telnetParameters(19200, 6, 1, UsbSerialPort.PARITY_NONE); + usbWrite(new byte[]{65, 1}); + data = telnetRead(2); + assertThat("19200/8N1", data, equalTo(new byte[]{1, 5})); + + // out 8N2: addddddd dooadddddddoo + // 1000001 0 1 + // in 6N1: addddddo addddddo + // 100000 1101 + usbParameters(19200, 8, UsbSerialPort.STOPBITS_2, UsbSerialPort.PARITY_NONE); + telnetParameters(19200, 6, 1, UsbSerialPort.PARITY_NONE); + usbWrite(new byte[]{65, 1}); + data = telnetRead(2); + assertThat("19200/8N1", data, equalTo(new byte[]{1, 11})); + // todo: could create similar test for 1.5 stopbits, by reading at double speed + // but only some devices support 1.5 stopbits and it is basically not used any more + } + } + + + @Test + public void probeTable() throws Exception { + class DummyDriver implements UsbSerialDriver { + @Override + public UsbDevice getDevice() { return null; } + @Override + public List getPorts() { return null; } + } + List availableDrivers; + ProbeTable probeTable = new ProbeTable(); + UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + availableDrivers = new UsbSerialProber(probeTable).findAllDrivers(usbManager); + assertEquals(0, availableDrivers.size()); + + probeTable.addProduct(0, 0, DummyDriver.class); + availableDrivers = new UsbSerialProber(probeTable).findAllDrivers(usbManager); + assertEquals(0, availableDrivers.size()); + + probeTable.addProduct(usbSerialDriver.getDevice().getVendorId(), usbSerialDriver.getDevice().getProductId(), usbSerialDriver.getClass()); + availableDrivers = new UsbSerialProber(probeTable).findAllDrivers(usbManager); + assertEquals(1, availableDrivers.size()); + assertEquals(true, availableDrivers.get(0).getClass() == usbSerialDriver.getClass()); + } + + @Test + // data loss es expected, if data is not consumed fast enough + public void readBuffer() throws Exception { + if(usbSerialDriver instanceof CdcAcmSerialDriver) + telnetWriteDelay = 10; // arduino_leonardo_bridge.ino sends each byte in own USB packet, which is horribly slow + usbParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE); + + StringBuilder expected = new StringBuilder(); + StringBuilder data = new StringBuilder(); + final int maxWait = 2000; + int bufferSize = 0; + for(bufferSize = 8; bufferSize < (2<<15); bufferSize *= 2) { + int linenr = 0; + String line; + expected.setLength(0); + data.setLength(0); + + Log.i(TAG, "bufferSize " + bufferSize); + usbReadBlock = true; + for (linenr = 0; linenr < bufferSize/8; linenr++) { + line = String.format("%06d\r\n", linenr); + telnetWrite(line.getBytes()); + expected.append(line); + } + usbReadBlock = false; + + // slowly write new data, until old data is comletely read from buffer and new data is received again + boolean found = false; + for (; linenr < bufferSize/8 + maxWait/10 && !found; linenr++) { + line = String.format("%06d\r\n", linenr); + telnetWrite(line.getBytes()); + Thread.sleep(10); + expected.append(line); + data.append(new String(usbRead(0))); + found = data.toString().endsWith(line); + } + if(!found) { + // use waiting read to clear input queue, else next test would see unexpected data + byte[] rest = null; + while(rest==null || rest.length>0) + rest = usbRead(-1); + fail("end not found"); + } + if (data.length() != expected.length()) + break; + } + int pos = StringUtils.indexOfDifference(data, expected); + Log.i(TAG, "bufferSize " + bufferSize + ", first difference at " + pos); + // actual values have large variance for same device, e.g. + // bufferSize 4096, first difference at 164 + // bufferSize 64, first difference at 57 + assertTrue(bufferSize > 16); + assertTrue(data.length() != expected.length()); + } + + @Test + // see logcat for performance results + public void readSpeed() throws Exception { + // CDC arduino_leonardo_bridge.ini has transfer speed ~ 100 byte/sec + // all other devices are near physical limit with ~ 10-12k/sec + + // CH340 w/o asyncReads (bulkTransfer) is much slower and fails reproducibly here + // FTDI w/o asyncReads (bulkTransfer) does not continue to read after ~2k + // CP2102 and PL2303 do not have data loss issues with bulkTransfer + usbParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE); + + // limited write ahead to avoid buffer overrun + // with unlimited write ahead all devices fail sporadically. is it windows/device/usb-buffer overrun? + int writeAhead = 2000; + if(usbSerialDriver instanceof CdcAcmSerialDriver) + writeAhead = 50; + + int linenr = 0; + String line=""; + StringBuilder data = new StringBuilder(); + StringBuilder expected = new StringBuilder(); + int dlen = 0, elen = 0; + Log.i(TAG, "readSpeed: 'in' should be near "+115200/10); + long begin = System.currentTimeMillis(); + long next = System.currentTimeMillis(); + for(int seconds=1; seconds<=5; seconds++) { + next += 1000; + while (System.currentTimeMillis() < next) { + if(expected.length() < data.length() + writeAhead) { + line = String.format("%06d\r\n", linenr++); + telnetWrite(line.getBytes()); + expected.append(line); + } else { + Thread.sleep(0, 100000); + } + data.append(new String(usbRead(0))); + } + Log.i(TAG, "readSpeed: t="+(next-begin)+", in="+(data.length()-dlen)+", out="+(expected.length()-elen)); + dlen = data.length(); + elen = expected.length(); + } + boolean found = false; + for (linenr=0; linenr < 2000 && !found; linenr++) { + data.append(new String(usbRead(0))); + Thread.sleep(1); + found = data.toString().endsWith(line); + } + next = System.currentTimeMillis(); + //Log.i(TAG, "readSpeed: t="+(next-begin)+", in="+(data.length()-dlen)); + assertTrue(found); + int pos = StringUtils.indexOfDifference(data, expected); + if(pos!=-1) { + Log.i(TAG, "readSpeed: first difference at " + pos); + String datasub = data.substring(Math.max(pos - 20, 0), Math.min(pos + 20, data.length())); + String expectedsub = expected.substring(Math.max(pos - 20, 0), Math.min(pos + 20, expected.length())); + assertThat(datasub, equalTo(expectedsub)); + } + } + + @Test + // see logcat for performance results + public void writeSpeed() throws Exception { + // CDC arduino_leonardo_bridge.ini has transfer speed ~ 100 byte/sec + // all other devices can get near physical limit: + // longlines=true:, speed is near physical limit at 11.5k + // longlines=false: speed is 3-4k for all devices, as more USB packets are required + usbParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE); + boolean longlines = !(usbSerialDriver instanceof CdcAcmSerialDriver); + + int linenr = 0; + String line=""; + StringBuilder data = new StringBuilder(); + StringBuilder expected = new StringBuilder(); + int dlen = 0, elen = 0; + Log.i(TAG, "writeSpeed: 'out' should be near "+115200/10); + long begin = System.currentTimeMillis(); + long next = System.currentTimeMillis(); + for(int seconds=1; seconds<=5; seconds++) { + next += 1000; + while (System.currentTimeMillis() < next) { + if(longlines) + line = String.format("%060d\r\n", linenr++); + else + line = String.format("%06d\r\n", linenr++); + usbWrite(line.getBytes()); + expected.append(line); + data.append(new String(telnetRead(0))); + } + Log.i(TAG, "writeSpeed: t="+(next-begin)+", out="+(expected.length()-elen)+", in="+(data.length()-dlen)); + dlen = data.length(); + elen = expected.length(); + } + boolean found = false; + for (linenr=0; linenr < 2000 && !found; linenr++) { + data.append(new String(telnetRead(0))); + Thread.sleep(1); + found = data.toString().endsWith(line); + } + next = System.currentTimeMillis(); + Log.i(TAG, "writeSpeed: t="+(next-begin)+", in="+(data.length()-dlen)); + assertTrue(found); + int pos = StringUtils.indexOfDifference(data, expected); + if(pos!=-1) { + Log.i(TAG, "writeSpeed: first difference at " + pos); + String datasub = data.substring(Math.max(pos - 20, 0), Math.min(pos + 20, data.length())); + String expectedsub = expected.substring(Math.max(pos - 20, 0), Math.min(pos + 20, expected.length())); + assertThat(datasub, equalTo(expectedsub)); + } + } + +} From 0ea5b282b76417ed8eeeb73d2a33be2ccff5585c Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Fri, 23 Mar 2018 22:58:33 +0100 Subject: [PATCH 2/3] implement async read for all devices --- .idea/.gitignore | 4 + build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- usbSerialExamples/build.gradle | 2 +- usbSerialForAndroid/build.gradle | 4 +- .../hoho/android/usbserial/DeviceTest.java | 204 ++++++++++++------ .../usbserial/driver/Ch34xSerialDriver.java | 30 +-- .../usbserial/driver/Cp21xxSerialDriver.java | 57 +++-- .../driver/ProlificSerialDriver.java | 47 +++- .../util/SerialInputOutputManager.java | 1 - 10 files changed, 251 insertions(+), 104 deletions(-) create mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..4a443ac --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,4 @@ +caches +codeStyles +libraries +workspace.xml diff --git a/build.gradle b/build.gradle index 17033eb..089111f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.1.1' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8f072ca..eb10a04 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 26 20:06:55 CEST 2017 +#Tue Mar 27 21:28:01 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/usbSerialExamples/build.gradle b/usbSerialExamples/build.gradle index cb2be2a..9fff8b3 100644 --- a/usbSerialExamples/build.gradle +++ b/usbSerialExamples/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 26 - buildToolsVersion "26.0.2" + buildToolsVersion '27.0.3' defaultConfig { minSdkVersion 14 diff --git a/usbSerialForAndroid/build.gradle b/usbSerialForAndroid/build.gradle index cd13792..db097a5 100644 --- a/usbSerialForAndroid/build.gradle +++ b/usbSerialForAndroid/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'signing' android { compileSdkVersion 26 - buildToolsVersion "26.0.2" + buildToolsVersion '27.0.3' defaultConfig { minSdkVersion 14 @@ -22,7 +22,7 @@ android { dependencies { testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support:support-annotations:27.1.0' + androidTestImplementation 'com.android.support:support-annotations:27.1.1' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'commons-net:commons-net:3.6' androidTestImplementation 'org.apache.commons:commons-lang3:3.7' diff --git a/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java index b0c48af..3b7abf9 100644 --- a/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java +++ b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java @@ -36,6 +36,7 @@ import android.hardware.usb.UsbManager; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.util.Log; +import android.os.Process; import com.hoho.android.usbserial.driver.CdcAcmSerialDriver; import com.hoho.android.usbserial.driver.Ch34xSerialDriver; @@ -48,7 +49,6 @@ import com.hoho.android.usbserial.driver.UsbSerialPort; import com.hoho.android.usbserial.driver.UsbSerialProber; import com.hoho.android.usbserial.util.SerialInputOutputManager; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.net.telnet.InvalidTelnetOptionException; import org.apache.commons.net.telnet.TelnetClient; import org.apache.commons.net.telnet.TelnetCommand; @@ -79,13 +79,15 @@ import static org.junit.Assert.fail; @RunWith(AndroidJUnit4.class) public class DeviceTest implements SerialInputOutputManager.Listener { - private final static String rfc2217_server_host = "192.168.0.171"; + private final static String rfc2217_server_host = "192.168.0.100"; private final static int rfc2217_server_port = 2217; - private final static boolean rfc2217_server_nonstandard_baudrates = false; // false on Windows + private final static boolean rfc2217_server_nonstandard_baudrates = false; // false on Windows, Raspi + private final static boolean rfc2217_server_parity_mark_space = false; // false on Raspi private final static int TELNET_READ_WAIT = 500; private final static int USB_READ_WAIT = 500; private final static int USB_WRITE_WAIT = 500; + private final static Integer SERIAL_INPUT_OUTPUT_MANAGER_THREAD_PRIORITY = Process.THREAD_PRIORITY_URGENT_AUDIO; private final static String TAG = "DeviceTest"; private final static byte RFC2217_COM_PORT_OPTION = 0x2c; @@ -101,6 +103,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { private SerialInputOutputManager usbIoManager; private final Deque usbReadBuffer = new LinkedList<>(); private boolean usbReadBlock = false; + private long usbReadTime = 0; private static TelnetClient telnetClient; private static InputStream telnetReadStream; @@ -137,7 +140,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { @Before public void setUp() throws Exception { setUpFixtureInt(); - telnetClient.sendAYT(1000); // not corrctly handled by rfc2217_server.py, but WARNING output "ignoring Telnet command: '\xf6'" is a nice separator between tests + telnetClient.sendAYT(1000); // not correctly handled by rfc2217_server.py, but WARNING output "ignoring Telnet command: '\xf6'" is a nice separator between tests telnetComPortOptionCounter[0] = 0; telnetWriteDelay = 0; @@ -175,7 +178,15 @@ public class DeviceTest implements SerialInputOutputManager.Listener { usbSerialPort.open(usbDeviceConnection); usbSerialPort.setDTR(true); usbSerialPort.setRTS(true); - usbIoManager = new SerialInputOutputManager(usbSerialPort, this); + usbIoManager = new SerialInputOutputManager(usbSerialPort, this) { + @Override + public void run() { + if(SERIAL_INPUT_OUTPUT_MANAGER_THREAD_PRIORITY != null) + Process.setThreadPriority(SERIAL_INPUT_OUTPUT_MANAGER_THREAD_PRIORITY); + super.run(); + } + }; + Executors.newSingleThreadExecutor().submit(usbIoManager); synchronized (usbReadBuffer) { @@ -263,7 +274,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { private byte[] usbRead(int expectedLength) throws Exception { long end = System.currentTimeMillis() + USB_READ_WAIT; - ByteBuffer buf = ByteBuffer.allocate(4096); + ByteBuffer buf = ByteBuffer.allocate(8192); if(usbIoManager != null) { while (System.currentTimeMillis() < end) { synchronized (usbReadBuffer) { @@ -335,6 +346,16 @@ public class DeviceTest implements SerialInputOutputManager.Listener { @Override public void onNewData(byte[] data) { + long now = System.currentTimeMillis(); + if(usbReadTime == 0) + usbReadTime = now; + if(data.length > 64) { + Log.d(TAG, "usb read: time+=" + String.format("%-3d",now-usbReadTime) + " len=" + String.format("%-4d",data.length) + " data=" + new String(data, 0, 32) + "..." + new String(data, data.length-32, 32)); + } else { + Log.d(TAG, "usb read: time+=" + String.format("%-3d",now-usbReadTime) + " len=" + String.format("%-4d",data.length) + " data=" + new String(data)); + } + usbReadTime = now; + while(usbReadBlock) try { Thread.sleep(1); @@ -351,6 +372,32 @@ public class DeviceTest implements SerialInputOutputManager.Listener { assertTrue("usb connection lost", false); } + // clone of org.apache.commons.lang3.StringUtils.indexOfDifference + optional startpos + private static int indexOfDifference(final CharSequence cs1, final CharSequence cs2) { + return indexOfDifference(cs1, cs2, 0, 0); + } + + private static int indexOfDifference(final CharSequence cs1, final CharSequence cs2, int cs1startpos, int cs2startpos) { + if (cs1 == cs2) { + return -1; + } + if (cs1 == null || cs2 == null) { + return 0; + } + if(cs1startpos < 0 || cs2startpos < 0) + return -1; + int i, j; + for (i = cs1startpos, j = cs2startpos; i < cs1.length() && j < cs2.length(); ++i, ++j) { + if (cs1.charAt(i) != cs2.charAt(j)) { + break; + } + } + if (j < cs2.length() || i < cs1.length()) { + return i; + } + return -1; + } + @Test public void baudRate() throws Exception { @@ -565,7 +612,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { if (usbSerialDriver instanceof CdcAcmSerialDriver) { // not supported by arduino_leonardo_bridge.ino, other devices might support it - } else { + } else if (rfc2217_server_parity_mark_space) { usbParameters(19200, 7, 1, UsbSerialPort.PARITY_MARK); usbWrite(_8n1); data = telnetRead(4); @@ -597,16 +644,17 @@ public class DeviceTest implements SerialInputOutputManager.Listener { if (usbSerialDriver instanceof CdcAcmSerialDriver) { // not supported by arduino_leonardo_bridge.ino, other devices might support it } else { - telnetParameters(19200, 7, 1, UsbSerialPort.PARITY_MARK); - telnetWrite(_8n1); - data = usbRead(4); - assertThat("19200/7M1", data, equalTo(_7m1)); - - telnetParameters(19200, 7, 1, UsbSerialPort.PARITY_SPACE); - telnetWrite(_8n1); - data = usbRead(4); - assertThat("19200/7S1", data, equalTo(_7s1)); + if (rfc2217_server_parity_mark_space) { + telnetParameters(19200, 7, 1, UsbSerialPort.PARITY_MARK); + telnetWrite(_8n1); + data = usbRead(4); + assertThat("19200/7M1", data, equalTo(_7m1)); + telnetParameters(19200, 7, 1, UsbSerialPort.PARITY_SPACE); + telnetWrite(_8n1); + data = usbRead(4); + assertThat("19200/7S1", data, equalTo(_7s1)); + } usbParameters(19200, 7, 1, UsbSerialPort.PARITY_ODD); telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); telnetWrite(_8n1); @@ -635,23 +683,23 @@ public class DeviceTest implements SerialInputOutputManager.Listener { // o - stop bit (1) // d - data bit - // out 8N2: addddddd doadddddddoo - // 1000001 0 1 - // in 6N1: addddddo adddddo - // 100000 101 + // out 8N2: addddddd doaddddddddo + // 1000001 0 10001111 + // in 6N1: addddddo addddddo + // 100000 101000 usbParameters(19200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); telnetParameters(19200, 6, 1, UsbSerialPort.PARITY_NONE); - usbWrite(new byte[]{65, 1}); + usbWrite(new byte[]{(byte)0x41, (byte)0xf1}); data = telnetRead(2); assertThat("19200/8N1", data, equalTo(new byte[]{1, 5})); - // out 8N2: addddddd dooadddddddoo - // 1000001 0 1 + // out 8N2: addddddd dooaddddddddoo + // 1000001 0 10011111 // in 6N1: addddddo addddddo - // 100000 1101 + // 100000 110100 usbParameters(19200, 8, UsbSerialPort.STOPBITS_2, UsbSerialPort.PARITY_NONE); telnetParameters(19200, 6, 1, UsbSerialPort.PARITY_NONE); - usbWrite(new byte[]{65, 1}); + usbWrite(new byte[]{(byte)0x41, (byte)0xf9}); data = telnetRead(2); assertThat("19200/8N1", data, equalTo(new byte[]{1, 11})); // todo: could create similar test for 1.5 stopbits, by reading at double speed @@ -705,7 +753,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { Log.i(TAG, "bufferSize " + bufferSize); usbReadBlock = true; for (linenr = 0; linenr < bufferSize/8; linenr++) { - line = String.format("%06d\r\n", linenr); + line = String.format("%07d,", linenr); telnetWrite(line.getBytes()); expected.append(line); } @@ -714,7 +762,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { // slowly write new data, until old data is comletely read from buffer and new data is received again boolean found = false; for (; linenr < bufferSize/8 + maxWait/10 && !found; linenr++) { - line = String.format("%06d\r\n", linenr); + line = String.format("%07d,", linenr); telnetWrite(line.getBytes()); Thread.sleep(10); expected.append(line); @@ -731,7 +779,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { if (data.length() != expected.length()) break; } - int pos = StringUtils.indexOfDifference(data, expected); + int pos = indexOfDifference(data, expected); Log.i(TAG, "bufferSize " + bufferSize + ", first difference at " + pos); // actual values have large variance for same device, e.g. // bufferSize 4096, first difference at 164 @@ -740,21 +788,30 @@ public class DeviceTest implements SerialInputOutputManager.Listener { assertTrue(data.length() != expected.length()); } + + // + // this test can fail sporadically! + // + // Android is not a real time OS, so there is no guarantee that the usb thread is scheduled, or it might be blocked by Java garbage collection. + // The SerialInputOutputManager uses a buffer size of 4Kb. Reading of these blocks happen behind UsbRequest.queue / UsbDeviceConnection.requestWait + // The dump of data and error positions in logcat show, that data is lost somewhere in the UsbRequest handling, + // very likely when the individual 64 byte USB packets are not read fast enough, and the serial converter chip has to discard bytes. + // + // On some days SERIAL_INPUT_OUTPUT_MANAGER_THREAD_PRIORITY=THREAD_PRIORITY_URGENT_AUDIO reduced errors by factor 10, on other days it had no effect at all! + // @Test - // see logcat for performance results public void readSpeed() throws Exception { + // see logcat for performance results + // // CDC arduino_leonardo_bridge.ini has transfer speed ~ 100 byte/sec // all other devices are near physical limit with ~ 10-12k/sec + int baudrate = 115200; + usbParameters(baudrate, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(baudrate, 8, 1, UsbSerialPort.PARITY_NONE); - // CH340 w/o asyncReads (bulkTransfer) is much slower and fails reproducibly here - // FTDI w/o asyncReads (bulkTransfer) does not continue to read after ~2k - // CP2102 and PL2303 do not have data loss issues with bulkTransfer - usbParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE); - telnetParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE); - - // limited write ahead to avoid buffer overrun - // with unlimited write ahead all devices fail sporadically. is it windows/device/usb-buffer overrun? - int writeAhead = 2000; + // fails more likely with larger or unlimited (-1) write ahead + int writeSeconds = 5; + int writeAhead = 5*baudrate/10; // write ahead for another 5 second read if(usbSerialDriver instanceof CdcAcmSerialDriver) writeAhead = 50; @@ -763,14 +820,14 @@ public class DeviceTest implements SerialInputOutputManager.Listener { StringBuilder data = new StringBuilder(); StringBuilder expected = new StringBuilder(); int dlen = 0, elen = 0; - Log.i(TAG, "readSpeed: 'in' should be near "+115200/10); + Log.i(TAG, "readSpeed: 'read' should be near "+baudrate/10); long begin = System.currentTimeMillis(); long next = System.currentTimeMillis(); - for(int seconds=1; seconds<=5; seconds++) { + for(int seconds=1; seconds <= writeSeconds; seconds++) { next += 1000; while (System.currentTimeMillis() < next) { - if(expected.length() < data.length() + writeAhead) { - line = String.format("%06d\r\n", linenr++); + if((writeAhead < 0) || (expected.length() < data.length() + writeAhead)) { + line = String.format("%07d,", linenr++); telnetWrite(line.getBytes()); expected.append(line); } else { @@ -778,31 +835,55 @@ public class DeviceTest implements SerialInputOutputManager.Listener { } data.append(new String(usbRead(0))); } - Log.i(TAG, "readSpeed: t="+(next-begin)+", in="+(data.length()-dlen)+", out="+(expected.length()-elen)); + Log.i(TAG, "readSpeed: t="+(next-begin)+", read="+(data.length()-dlen)+", write="+(expected.length()-elen)); dlen = data.length(); elen = expected.length(); } boolean found = false; - for (linenr=0; linenr < 2000 && !found; linenr++) { + long maxwait = Math.max(1000, (expected.length() - data.length()) * 20000L / baudrate ); + next = System.currentTimeMillis() + maxwait; + Log.d(TAG, "readSpeed: rest wait time " + maxwait + " for " + (expected.length() - data.length()) + " byte"); + while(!found && System.currentTimeMillis() < next) { data.append(new String(usbRead(0))); - Thread.sleep(1); found = data.toString().endsWith(line); + Thread.sleep(1); } - next = System.currentTimeMillis(); - //Log.i(TAG, "readSpeed: t="+(next-begin)+", in="+(data.length()-dlen)); - assertTrue(found); - int pos = StringUtils.indexOfDifference(data, expected); - if(pos!=-1) { - Log.i(TAG, "readSpeed: first difference at " + pos); - String datasub = data.substring(Math.max(pos - 20, 0), Math.min(pos + 20, data.length())); - String expectedsub = expected.substring(Math.max(pos - 20, 0), Math.min(pos + 20, expected.length())); - assertThat(datasub, equalTo(expectedsub)); + //next = System.currentTimeMillis(); + //Log.i(TAG, "readSpeed: t="+(next-begin)+", read="+(data.length()-dlen)); + + int errcnt = 0; + int errlen = 0; + int datapos = indexOfDifference(data, expected); + int expectedpos = datapos; + while(datapos != -1) { + errcnt += 1; + int nextexpectedpos = -1; + int nextdatapos = datapos + 2; + int len = -1; + if(nextdatapos + 10 < data.length()) { // try to sync data+expected, assuming that data is lost, but not corrupted + String nextsub = data.substring(nextdatapos, nextdatapos + 10); + nextexpectedpos = expected.indexOf(nextsub, expectedpos); + if(nextexpectedpos >= 0) { + len = nextexpectedpos - expectedpos - 2; + errlen += len; + } + } + Log.i(TAG, "readSpeed: difference at " + datapos + " len " + len ); + Log.d(TAG, "readSpeed: got " + data.substring(Math.max(datapos - 20, 0), Math.min(datapos + 20, data.length()))); + Log.d(TAG, "readSpeed: expected " + expected.substring(Math.max(expectedpos - 20, 0), Math.min(expectedpos + 20, expected.length()))); + datapos = indexOfDifference(data, expected, nextdatapos, nextexpectedpos); + expectedpos = nextexpectedpos + (datapos - nextdatapos); } + if(errcnt != 0) + Log.i(TAG, "readSpeed: got " + errcnt + " errors, total len " + errlen+ ", avg. len " + errlen/errcnt); + assertTrue("end not found", found); + assertEquals("no errors", 0, errcnt); } @Test - // see logcat for performance results public void writeSpeed() throws Exception { + // see logcat for performance results + // // CDC arduino_leonardo_bridge.ini has transfer speed ~ 100 byte/sec // all other devices can get near physical limit: // longlines=true:, speed is near physical limit at 11.5k @@ -816,21 +897,21 @@ public class DeviceTest implements SerialInputOutputManager.Listener { StringBuilder data = new StringBuilder(); StringBuilder expected = new StringBuilder(); int dlen = 0, elen = 0; - Log.i(TAG, "writeSpeed: 'out' should be near "+115200/10); + Log.i(TAG, "writeSpeed: 'write' should be near "+115200/10); long begin = System.currentTimeMillis(); long next = System.currentTimeMillis(); for(int seconds=1; seconds<=5; seconds++) { next += 1000; while (System.currentTimeMillis() < next) { if(longlines) - line = String.format("%060d\r\n", linenr++); + line = String.format("%060d,", linenr++); else - line = String.format("%06d\r\n", linenr++); + line = String.format("%07d,", linenr++); usbWrite(line.getBytes()); expected.append(line); data.append(new String(telnetRead(0))); } - Log.i(TAG, "writeSpeed: t="+(next-begin)+", out="+(expected.length()-elen)+", in="+(data.length()-dlen)); + Log.i(TAG, "writeSpeed: t="+(next-begin)+", write="+(expected.length()-elen)+", read="+(data.length()-dlen)); dlen = data.length(); elen = expected.length(); } @@ -841,10 +922,11 @@ public class DeviceTest implements SerialInputOutputManager.Listener { found = data.toString().endsWith(line); } next = System.currentTimeMillis(); - Log.i(TAG, "writeSpeed: t="+(next-begin)+", in="+(data.length()-dlen)); + Log.i(TAG, "writeSpeed: t="+(next-begin)+", read="+(data.length()-dlen)); assertTrue(found); - int pos = StringUtils.indexOfDifference(data, expected); + int pos = indexOfDifference(data, expected); if(pos!=-1) { + Log.i(TAG, "writeSpeed: first difference at " + pos); String datasub = data.substring(Math.max(pos - 20, 0), Math.min(pos + 20, data.length())); String expectedsub = expected.substring(Math.max(pos - 20, 0), Math.min(pos + 20, expected.length())); diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Ch34xSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Ch34xSerialDriver.java index 59be14f..7c8115c 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Ch34xSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Ch34xSerialDriver.java @@ -191,23 +191,23 @@ public class Ch34xSerialDriver implements UsbSerialDriver { } finally { request.close(); } - } - - final int numBytesRead; - synchronized (mReadBufferLock) { - int readAmt = Math.min(dest.length, mReadBuffer.length); - numBytesRead = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, readAmt, - timeoutMillis); - if (numBytesRead < 0) { - // This sucks: we get -1 on timeout, not 0 as preferred. - // We *should* use UsbRequest, except it has a bug/api oversight - // where there is no way to determine the number of bytes read - // in response :\ -- http://b.android.com/28023 - return 0; + } else { + final int numBytesRead; + synchronized (mReadBufferLock) { + int readAmt = Math.min(dest.length, mReadBuffer.length); + numBytesRead = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, readAmt, + timeoutMillis); + if (numBytesRead < 0) { + // This sucks: we get -1 on timeout, not 0 as preferred. + // We *should* use UsbRequest, except it has a bug/api oversight + // where there is no way to determine the number of bytes read + // in response :\ -- http://b.android.com/28023 + return 0; + } + System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead); } - System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead); + return numBytesRead; } - return numBytesRead; } @Override diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Cp21xxSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Cp21xxSerialDriver.java index b5956a8..e57d96a 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Cp21xxSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Cp21xxSerialDriver.java @@ -26,9 +26,12 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbRequest; +import android.os.Build; import android.util.Log; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -101,11 +104,13 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { private static final int CONTROL_WRITE_DTR = 0x0100; private static final int CONTROL_WRITE_RTS = 0x0200; + private final boolean mEnableAsyncReads; private UsbEndpoint mReadEndpoint; private UsbEndpoint mWriteEndpoint; public Cp21xxSerialPort(UsbDevice device, int portNumber) { super(device, portNumber); + mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1); } @Override @@ -179,21 +184,47 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { @Override public int read(byte[] dest, int timeoutMillis) throws IOException { - final int numBytesRead; - synchronized (mReadBufferLock) { - int readAmt = Math.min(dest.length, mReadBuffer.length); - numBytesRead = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, readAmt, - timeoutMillis); - if (numBytesRead < 0) { - // This sucks: we get -1 on timeout, not 0 as preferred. - // We *should* use UsbRequest, except it has a bug/api oversight - // where there is no way to determine the number of bytes read - // in response :\ -- http://b.android.com/28023 - return 0; + if (mEnableAsyncReads) { + final UsbRequest request = new UsbRequest(); + try { + request.initialize(mConnection, mReadEndpoint); + final ByteBuffer buf = ByteBuffer.wrap(dest); + if (!request.queue(buf, dest.length)) { + throw new IOException("Error queueing request."); + } + + final UsbRequest response = mConnection.requestWait(); + if (response == null) { + throw new IOException("Null response"); + } + + final int nread = buf.position(); + if (nread > 0) { + //Log.d(TAG, HexDump.dumpHexString(dest, 0, Math.min(32, dest.length))); + return nread; + } else { + return 0; + } + } finally { + request.close(); } - System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead); + } else { + final int numBytesRead; + synchronized (mReadBufferLock) { + int readAmt = Math.min(dest.length, mReadBuffer.length); + numBytesRead = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, readAmt, + timeoutMillis); + if (numBytesRead < 0) { + // This sucks: we get -1 on timeout, not 0 as preferred. + // We *should* use UsbRequest, except it has a bug/api oversight + // where there is no way to determine the number of bytes read + // in response :\ -- http://b.android.com/28023 + return 0; + } + System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead); + } + return numBytesRead; } - return numBytesRead; } @Override diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/ProlificSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/ProlificSerialDriver.java index 01d3a85..550350c 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/ProlificSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/ProlificSerialDriver.java @@ -32,10 +32,13 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbRequest; +import android.os.Build; import android.util.Log; import java.io.IOException; import java.lang.reflect.Method; +import java.nio.ByteBuffer; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -109,6 +112,7 @@ public class ProlificSerialDriver implements UsbSerialDriver { private int mDeviceType = DEVICE_TYPE_HX; + private final boolean mEnableAsyncReads; private UsbEndpoint mReadEndpoint; private UsbEndpoint mWriteEndpoint; private UsbEndpoint mInterruptEndpoint; @@ -126,6 +130,7 @@ public class ProlificSerialDriver implements UsbSerialDriver { public ProlificSerialPort(UsbDevice device, int portNumber) { super(device, portNumber); + mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1); } @Override @@ -368,15 +373,41 @@ public class ProlificSerialDriver implements UsbSerialDriver { @Override public int read(byte[] dest, int timeoutMillis) throws IOException { - synchronized (mReadBufferLock) { - int readAmt = Math.min(dest.length, mReadBuffer.length); - int numBytesRead = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, - readAmt, timeoutMillis); - if (numBytesRead < 0) { - return 0; + if (mEnableAsyncReads) { + final UsbRequest request = new UsbRequest(); + try { + request.initialize(mConnection, mReadEndpoint); + final ByteBuffer buf = ByteBuffer.wrap(dest); + if (!request.queue(buf, dest.length)) { + throw new IOException("Error queueing request."); + } + + final UsbRequest response = mConnection.requestWait(); + if (response == null) { + throw new IOException("Null response"); + } + + final int nread = buf.position(); + if (nread > 0) { + //Log.d(TAG, HexDump.dumpHexString(dest, 0, Math.min(32, dest.length))); + return nread; + } else { + return 0; + } + } finally { + request.close(); + } + } else { + synchronized (mReadBufferLock) { + int readAmt = Math.min(dest.length, mReadBuffer.length); + int numBytesRead = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, + readAmt, timeoutMillis); + if (numBytesRead < 0) { + return 0; + } + System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead); + return numBytesRead; } - System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead); - return numBytesRead; } } diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/util/SerialInputOutputManager.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/util/SerialInputOutputManager.java index 51c5655..78bcd0b 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/util/SerialInputOutputManager.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/util/SerialInputOutputManager.java @@ -120,7 +120,6 @@ public class SerialInputOutputManager implements Runnable { * called, or until a driver exception is raised. * * NOTE(mikey): Uses inefficient read/write-with-timeout. - * TODO(mikey): Read asynchronously with {@link UsbRequest#queue(ByteBuffer, int)} */ @Override public void run() { From 61b272b8b6e087f0bc9b065eef1e7baf66412fa7 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Thu, 10 May 2018 20:28:26 +0200 Subject: [PATCH 3/3] support ft_232h, cp210_ multiport devices harmonize claimInterface() error handling cancel read() on close() --- .idea/.name | 1 - .idea/compiler.xml | 21 ------ .idea/copyright/profiles_settings.xml | 3 - .idea/misc.xml | 25 ++----- build.gradle | 2 +- usbSerialExamples/build.gradle | 4 +- usbSerialForAndroid/build.gradle | 6 +- .../hoho/android/usbserial/DeviceTest.java | 73 ++++++++++++++----- .../usbserial/driver/Ch34xSerialDriver.java | 20 +++-- .../usbserial/driver/Cp21xxSerialDriver.java | 61 +++++++++++----- .../usbserial/driver/FtdiSerialDriver.java | 65 +++++++++-------- .../hoho/android/usbserial/driver/UsbId.java | 3 + 12 files changed, 161 insertions(+), 123 deletions(-) delete mode 100644 .idea/.name delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/copyright/profiles_settings.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 88cdcce..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -usb-serial-for-android \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 1f2af51..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf3..0000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 13c4629..c0f68ed 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,11 +5,12 @@ @@ -24,26 +25,10 @@ - + - - - - - 1.8 - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 089111f..df159ef 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.1' + classpath 'com.android.tools.build:gradle:3.1.2' } } diff --git a/usbSerialExamples/build.gradle b/usbSerialExamples/build.gradle index 9fff8b3..cde7b10 100644 --- a/usbSerialExamples/build.gradle +++ b/usbSerialExamples/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 26 + compileSdkVersion 27 buildToolsVersion '27.0.3' defaultConfig { minSdkVersion 14 - targetSdkVersion 26 + targetSdkVersion 27 testInstrumentationRunner "android.test.InstrumentationTestRunner" } diff --git a/usbSerialForAndroid/build.gradle b/usbSerialForAndroid/build.gradle index db097a5..d335842 100644 --- a/usbSerialForAndroid/build.gradle +++ b/usbSerialForAndroid/build.gradle @@ -3,12 +3,12 @@ apply plugin: 'maven' apply plugin: 'signing' android { - compileSdkVersion 26 + compileSdkVersion 27 buildToolsVersion '27.0.3' defaultConfig { minSdkVersion 14 - targetSdkVersion 26 + targetSdkVersion 27 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } @@ -23,7 +23,7 @@ android { dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support:support-annotations:27.1.1' - androidTestImplementation 'com.android.support.test:runner:1.0.1' + androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'commons-net:commons-net:3.6' androidTestImplementation 'org.apache.commons:commons-lang3:3.7' } diff --git a/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java index 3b7abf9..e20ab5e 100644 --- a/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java +++ b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java @@ -79,10 +79,12 @@ import static org.junit.Assert.fail; @RunWith(AndroidJUnit4.class) public class DeviceTest implements SerialInputOutputManager.Listener { + // configuration: private final static String rfc2217_server_host = "192.168.0.100"; private final static int rfc2217_server_port = 2217; private final static boolean rfc2217_server_nonstandard_baudrates = false; // false on Windows, Raspi private final static boolean rfc2217_server_parity_mark_space = false; // false on Raspi + private final static int test_device_port = 0; private final static int TELNET_READ_WAIT = 500; private final static int USB_READ_WAIT = 500; @@ -110,6 +112,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { private static OutputStream telnetWriteStream; private static Integer[] telnetComPortOptionCounter = {0}; private int telnetWriteDelay = 0; + private boolean isCp21xxRestrictedPort = false; // second port of Cp2105 has limited dataBits, stopBits, parity @BeforeClass public static void setUpFixture() throws Exception { @@ -149,10 +152,10 @@ public class DeviceTest implements SerialInputOutputManager.Listener { List availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager); assertEquals("no usb device found", 1, availableDrivers.size()); usbSerialDriver = availableDrivers.get(0); - assertEquals(1, usbSerialDriver.getPorts().size()); - usbSerialPort = usbSerialDriver.getPorts().get(0); + assertTrue( usbSerialDriver.getPorts().size() > test_device_port); + usbSerialPort = usbSerialDriver.getPorts().get(test_device_port); Log.i(TAG, "Using USB device "+ usbSerialDriver.getClass().getSimpleName()); - + isCp21xxRestrictedPort = usbSerialDriver instanceof Cp21xxSerialDriver && usbSerialDriver.getPorts().size()==2 && test_device_port == 1; if (!usbManager.hasPermission(usbSerialPort.getDriver().getDevice())) { final Boolean[] granted = {Boolean.FALSE}; @@ -432,6 +435,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { ; // todo: add range check in driver else fail("invalid baudrate 0"); + } catch (java.io.IOException e) { // cp2105 second port } catch (java.lang.IllegalArgumentException e) { } try { @@ -445,6 +449,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { else fail("invalid baudrate 0"); } catch (java.lang.ArithmeticException e) { // ch340 + } catch (java.io.IOException e) { // cp2105 second port } catch (java.lang.IllegalArgumentException e) { } try { @@ -473,12 +478,21 @@ public class DeviceTest implements SerialInputOutputManager.Listener { else fail("invalid baudrate 2^31"); } catch (java.lang.ArithmeticException e) { // ch340 + } catch (java.io.IOException e) { // cp2105 second port } catch (java.lang.IllegalArgumentException e) { } - for(int baudRate : new int[] {2400, 19200, 42000, 115200} ) { + for(int baudRate : new int[] {300, 2400, 19200, 42000, 115200} ) { if(baudRate == 42000 && !rfc2217_server_nonstandard_baudrates) continue; // rfc2217_server.py would terminate + if(baudRate == 300 && isCp21xxRestrictedPort) { + try { + usbParameters(baudRate, 8, 1, UsbSerialPort.PARITY_NONE); + assertTrue(false); + } catch (java.io.IOException e) { + } + continue; + } telnetParameters(baudRate, 8, 1, UsbSerialPort.PARITY_NONE); usbParameters(baudRate, 8, 1, UsbSerialPort.PARITY_NONE); @@ -543,14 +557,18 @@ public class DeviceTest implements SerialInputOutputManager.Listener { assertThat("19000/5N1", data, equalTo(new byte[] {(byte)0xe0, (byte)0xff})); // usb -> telnet - telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); - usbParameters(19200, 7, 1, UsbSerialPort.PARITY_NONE); - usbWrite(new byte[] {0x00}); - Thread.sleep(1); - usbWrite(new byte[] {(byte)0xff}); - data = telnetRead(2); - assertThat("19000/7N1", data, equalTo(new byte[] {(byte)0x80, (byte)0xff})); - + try { + telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + usbParameters(19200, 7, 1, UsbSerialPort.PARITY_NONE); + usbWrite(new byte[]{0x00}); + Thread.sleep(1); + usbWrite(new byte[]{(byte) 0xff}); + data = telnetRead(2); + assertThat("19000/7N1", data, equalTo(new byte[]{(byte) 0x80, (byte) 0xff})); + } catch (java.lang.IllegalArgumentException e) { + if(!isCp21xxRestrictedPort) + throw e; + } try { usbParameters(19200, 6, 1, UsbSerialPort.PARITY_NONE); usbWrite(new byte[]{0x00}); @@ -559,7 +577,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { data = telnetRead(2); assertThat("19000/6N1", data, equalTo(new byte[]{(byte) 0xc0, (byte) 0xff})); } catch (java.lang.IllegalArgumentException e) { - if (!(usbSerialDriver instanceof FtdiSerialDriver)) + if (!(isCp21xxRestrictedPort || usbSerialDriver instanceof FtdiSerialDriver)) throw e; } try { @@ -570,7 +588,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { data = telnetRead(2); assertThat("19000/5N1", data, equalTo(new byte[] {(byte)0xe0, (byte)0xff})); } catch (java.lang.IllegalArgumentException e) { - if (!(usbSerialDriver instanceof FtdiSerialDriver)) + if (!(isCp21xxRestrictedPort || usbSerialDriver instanceof FtdiSerialDriver)) throw e; } } @@ -592,6 +610,18 @@ public class DeviceTest implements SerialInputOutputManager.Listener { } catch (java.lang.IllegalArgumentException e) { } } + if(isCp21xxRestrictedPort) { + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_EVEN); + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_ODD); + try { + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_MARK); + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_SPACE); + } catch (java.lang.IllegalArgumentException e) { + } + return; + // test below not possible as it requires unsupported 7 dataBits + } // usb -> telnet telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); @@ -697,11 +727,16 @@ public class DeviceTest implements SerialInputOutputManager.Listener { // 1000001 0 10011111 // in 6N1: addddddo addddddo // 100000 110100 - usbParameters(19200, 8, UsbSerialPort.STOPBITS_2, UsbSerialPort.PARITY_NONE); - telnetParameters(19200, 6, 1, UsbSerialPort.PARITY_NONE); - usbWrite(new byte[]{(byte)0x41, (byte)0xf9}); - data = telnetRead(2); - assertThat("19200/8N1", data, equalTo(new byte[]{1, 11})); + try { + usbParameters(19200, 8, UsbSerialPort.STOPBITS_2, UsbSerialPort.PARITY_NONE); + telnetParameters(19200, 6, 1, UsbSerialPort.PARITY_NONE); + usbWrite(new byte[]{(byte) 0x41, (byte) 0xf9}); + data = telnetRead(2); + assertThat("19200/8N1", data, equalTo(new byte[]{1, 11})); + } catch(java.lang.IllegalArgumentException e) { + if(!isCp21xxRestrictedPort) + throw e; + } // todo: could create similar test for 1.5 stopbits, by reading at double speed // but only some devices support 1.5 stopbits and it is basically not used any more } diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Ch34xSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Ch34xSerialDriver.java index 7c8115c..48b26fc 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Ch34xSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Ch34xSerialDriver.java @@ -84,9 +84,10 @@ public class Ch34xSerialDriver implements UsbSerialDriver { private boolean dtr = false; private boolean rts = false; - private final boolean mEnableAsyncReads; + private final Boolean mEnableAsyncReads; private UsbEndpoint mReadEndpoint; private UsbEndpoint mWriteEndpoint; + private UsbRequest mUsbRequest; public Ch340SerialPort(UsbDevice device, int portNumber) { super(device, portNumber); @@ -109,10 +110,8 @@ public class Ch34xSerialDriver implements UsbSerialDriver { try { for (int i = 0; i < mDevice.getInterfaceCount(); i++) { UsbInterface usbIface = mDevice.getInterface(i); - if (mConnection.claimInterface(usbIface, true)) { - Log.d(TAG, "claimInterface " + i + " SUCCESS"); - } else { - Log.d(TAG, "claimInterface " + i + " FAIL"); + if (!mConnection.claimInterface(usbIface, true)) { + throw new IOException("Could not claim data interface."); } } @@ -154,7 +153,10 @@ public class Ch34xSerialDriver implements UsbSerialDriver { if (mConnection == null) { throw new IOException("Already closed"); } - + synchronized (mEnableAsyncReads) { + if (mUsbRequest != null) + mUsbRequest.cancel(); + } // TODO: nothing sended on close, maybe needed? try { @@ -175,8 +177,11 @@ public class Ch34xSerialDriver implements UsbSerialDriver { if (!request.queue(buf, dest.length)) { throw new IOException("Error queueing request."); } - + mUsbRequest = request; final UsbRequest response = mConnection.requestWait(); + synchronized (mEnableAsyncReads) { + mUsbRequest = null; + } if (response == null) { throw new IOException("Null response"); } @@ -189,6 +194,7 @@ public class Ch34xSerialDriver implements UsbSerialDriver { return 0; } } finally { + mUsbRequest = null; request.close(); } } else { diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Cp21xxSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Cp21xxSerialDriver.java index e57d96a..2119130 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Cp21xxSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/Cp21xxSerialDriver.java @@ -32,7 +32,7 @@ import android.util.Log; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Collections; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -42,11 +42,14 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { private static final String TAG = Cp21xxSerialDriver.class.getSimpleName(); private final UsbDevice mDevice; - private final UsbSerialPort mPort; + private final List mPorts; public Cp21xxSerialDriver(UsbDevice device) { mDevice = device; - mPort = new Cp21xxSerialPort(mDevice, 0); + mPorts = new ArrayList<>(); + for( int port = 0; port < device.getInterfaceCount(); port++) { + mPorts.add(new Cp21xxSerialPort(mDevice, port)); + } } @Override @@ -56,7 +59,7 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { @Override public List getPorts() { - return Collections.singletonList(mPort); + return mPorts; } public class Cp21xxSerialPort extends CommonUsbSerialPort { @@ -104,9 +107,14 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { private static final int CONTROL_WRITE_DTR = 0x0100; private static final int CONTROL_WRITE_RTS = 0x0200; - private final boolean mEnableAsyncReads; + private final Boolean mEnableAsyncReads; private UsbEndpoint mReadEndpoint; private UsbEndpoint mWriteEndpoint; + private UsbRequest mUsbRequest; + + // second port of Cp2105 has limited baudRate, dataBits, stopBits, parity + // unsupported baudrate returns error at controlTransfer(), other parameters are silently ignored + private boolean mIsRestrictedPort; public Cp21xxSerialPort(UsbDevice device, int portNumber) { super(device, portNumber); @@ -120,7 +128,7 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { private int setConfigSingle(int request, int value) { return mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, request, value, - 0, null, 0, USB_WRITE_TIMEOUT_MILLIS); + mPortNumber, null, 0, USB_WRITE_TIMEOUT_MILLIS); } @Override @@ -131,17 +139,15 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { mConnection = connection; boolean opened = false; + mIsRestrictedPort = mDevice.getInterfaceCount() == 2 && mPortNumber == 1; try { - for (int i = 0; i < mDevice.getInterfaceCount(); i++) { - UsbInterface usbIface = mDevice.getInterface(i); - if (mConnection.claimInterface(usbIface, true)) { - Log.d(TAG, "claimInterface " + i + " SUCCESS"); - } else { - Log.d(TAG, "claimInterface " + i + " FAIL"); - } + if(mPortNumber >= mDevice.getInterfaceCount()) { + throw new IOException("Unknown port number"); + } + UsbInterface dataIface = mDevice.getInterface(mPortNumber); + if (!mConnection.claimInterface(dataIface, true)) { + throw new IOException("Could not claim interface " + mPortNumber); } - - UsbInterface dataIface = mDevice.getInterface(mDevice.getInterfaceCount() - 1); for (int i = 0; i < dataIface.getEndpointCount(); i++) { UsbEndpoint ep = dataIface.getEndpoint(i); if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) { @@ -174,6 +180,11 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { if (mConnection == null) { throw new IOException("Already closed"); } + synchronized (mEnableAsyncReads) { + if(mUsbRequest != null) { + mUsbRequest.cancel(); + } + } try { setConfigSingle(SILABSER_IFC_ENABLE_REQUEST_CODE, UART_DISABLE); mConnection.close(); @@ -192,8 +203,11 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { if (!request.queue(buf, dest.length)) { throw new IOException("Error queueing request."); } - + mUsbRequest = request; final UsbRequest response = mConnection.requestWait(); + synchronized (mEnableAsyncReads) { + mUsbRequest = null; + } if (response == null) { throw new IOException("Null response"); } @@ -206,6 +220,7 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { return 0; } } finally { + mUsbRequest = null; request.close(); } } else { @@ -269,7 +284,7 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { (byte) ((baudRate >> 24) & 0xff) }; int ret = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SILABSER_SET_BAUDRATE, - 0, 0, data, 4, USB_WRITE_TIMEOUT_MILLIS); + 0, mPortNumber, data, 4, USB_WRITE_TIMEOUT_MILLIS); if (ret < 0) { throw new IOException("Error setting baud rate."); } @@ -283,12 +298,18 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { int configDataBits = 0; switch (dataBits) { case DATABITS_5: + if(mIsRestrictedPort) + throw new IllegalArgumentException("Unsupported dataBits value: " + dataBits); configDataBits |= 0x0500; break; case DATABITS_6: + if(mIsRestrictedPort) + throw new IllegalArgumentException("Unsupported dataBits value: " + dataBits); configDataBits |= 0x0600; break; case DATABITS_7: + if(mIsRestrictedPort) + throw new IllegalArgumentException("Unsupported dataBits value: " + dataBits); configDataBits |= 0x0700; break; case DATABITS_8: @@ -308,9 +329,13 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { configDataBits |= 0x0020; break; case PARITY_MARK: + if(mIsRestrictedPort) + throw new IllegalArgumentException("Unsupported parity value: mark"); configDataBits |= 0x0030; break; case PARITY_SPACE: + if(mIsRestrictedPort) + throw new IllegalArgumentException("Unsupported parity value: space"); configDataBits |= 0x0040; break; default: @@ -323,6 +348,8 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { case STOPBITS_1_5: throw new IllegalArgumentException("Unsupported stopBits value: 1.5"); case STOPBITS_2: + if(mIsRestrictedPort) + throw new IllegalArgumentException("Unsupported stopBits value: 2"); configDataBits |= 2; break; default: diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/FtdiSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/FtdiSerialDriver.java index 56b98b8..757c567 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/FtdiSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/FtdiSerialDriver.java @@ -29,11 +29,9 @@ import android.hardware.usb.UsbRequest; import android.os.Build; import android.util.Log; -import com.hoho.android.usbserial.util.HexDump; - import java.io.IOException; import java.nio.ByteBuffer; -import java.util.Collections; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -73,6 +71,8 @@ import java.util.Map; * Supported and tested devices: *
    *
  • {@value DeviceType#TYPE_R}
  • + *
  • {@value DeviceType#TYPE_2232H}
  • + *
  • {@value DeviceType#TYPE_4232H}
  • *
*

*

@@ -80,8 +80,6 @@ import java.util.Map; * feedback or patches): *

    *
  • {@value DeviceType#TYPE_2232C}
  • - *
  • {@value DeviceType#TYPE_2232H}
  • - *
  • {@value DeviceType#TYPE_4232H}
  • *
  • {@value DeviceType#TYPE_AM}
  • *
  • {@value DeviceType#TYPE_BM}
  • *
@@ -96,7 +94,7 @@ import java.util.Map; public class FtdiSerialDriver implements UsbSerialDriver { private final UsbDevice mDevice; - private final UsbSerialPort mPort; + private final List mPorts; /** * FTDI chip types. @@ -107,8 +105,12 @@ public class FtdiSerialDriver implements UsbSerialDriver { public FtdiSerialDriver(UsbDevice device) { mDevice = device; - mPort = new FtdiSerialPort(mDevice, 0); + mPorts = new ArrayList<>(); + for( int port = 0; port < device.getInterfaceCount(); port++) { + mPorts.add(new FtdiSerialPort(mDevice, port)); + } } + @Override public UsbDevice getDevice() { return mDevice; @@ -116,7 +118,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { @Override public List getPorts() { - return Collections.singletonList(mPort); + return mPorts; } private class FtdiSerialPort extends CommonUsbSerialPort { @@ -182,9 +184,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { private DeviceType mType; - private int mInterface = 0; /* INTERFACE_ANY */ - - private int mMaxPacketSize = 64; // TODO(mikey): detect + private int mIndex = 0; /** * Due to http://b.android.com/28023 , we cannot use UsbRequest async reads @@ -230,14 +230,21 @@ public class FtdiSerialDriver implements UsbSerialDriver { } public void reset() throws IOException { + // TODO(mikey): autodetect. + mType = DeviceType.TYPE_R; + if(mDevice.getInterfaceCount() > 1) { + mIndex = mPortNumber + 1; + if (mDevice.getInterfaceCount() == 2) + mType = DeviceType.TYPE_2232H; + if (mDevice.getInterfaceCount() == 4) + mType = DeviceType.TYPE_4232H; + } + int result = mConnection.controlTransfer(FTDI_DEVICE_OUT_REQTYPE, SIO_RESET_REQUEST, - SIO_RESET_SIO, 0 /* index */, null, 0, USB_WRITE_TIMEOUT_MILLIS); + SIO_RESET_SIO, mIndex, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Reset failed: result=" + result); } - - // TODO(mikey): autodetect. - mType = DeviceType.TYPE_R; } @Override @@ -249,12 +256,10 @@ public class FtdiSerialDriver implements UsbSerialDriver { boolean opened = false; try { - for (int i = 0; i < mDevice.getInterfaceCount(); i++) { - if (connection.claimInterface(mDevice.getInterface(i), true)) { - Log.d(TAG, "claimInterface " + i + " SUCCESS"); - } else { - throw new IOException("Error claiming interface " + i); - } + if (connection.claimInterface(mDevice.getInterface(mPortNumber), true)) { + Log.d(TAG, "claimInterface " + mPortNumber + " SUCCESS"); + } else { + throw new IOException("Error claiming interface " + mPortNumber); } reset(); opened = true; @@ -280,7 +285,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { @Override public int read(byte[] dest, int timeoutMillis) throws IOException { - final UsbEndpoint endpoint = mDevice.getInterface(0).getEndpoint(0); + final UsbEndpoint endpoint = mDevice.getInterface(mPortNumber).getEndpoint(0); if (mEnableAsyncReads) { final UsbRequest request = new UsbRequest(); @@ -325,7 +330,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { @Override public int write(byte[] src, int timeoutMillis) throws IOException { - final UsbEndpoint endpoint = mDevice.getInterface(0).getEndpoint(1); + final UsbEndpoint endpoint = mDevice.getInterface(mPortNumber).getEndpoint(1); int offset = 0; while (offset < src.length) { @@ -425,7 +430,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { } int result = mConnection.controlTransfer(FTDI_DEVICE_OUT_REQTYPE, - SIO_SET_DATA_REQUEST, config, 0 /* index */, + SIO_SET_DATA_REQUEST, config, mIndex, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Setting parameters failed: result=" + result); @@ -505,9 +510,8 @@ public class FtdiSerialDriver implements UsbSerialDriver { long index; if (mType == DeviceType.TYPE_2232C || mType == DeviceType.TYPE_2232H || mType == DeviceType.TYPE_4232H) { - index = (encodedDivisor >> 8) & 0xffff; - index &= 0xFF00; - index |= 0 /* TODO mIndex */; + index = (encodedDivisor >> 8) & 0xff00; + index |= mIndex; } else { index = (encodedDivisor >> 16) & 0xffff; } @@ -560,7 +564,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { public boolean purgeHwBuffers(boolean purgeReadBuffers, boolean purgeWriteBuffers) throws IOException { if (purgeReadBuffers) { int result = mConnection.controlTransfer(FTDI_DEVICE_OUT_REQTYPE, SIO_RESET_REQUEST, - SIO_RESET_PURGE_RX, 0 /* index */, null, 0, USB_WRITE_TIMEOUT_MILLIS); + SIO_RESET_PURGE_RX, mIndex, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Flushing RX failed: result=" + result); } @@ -568,7 +572,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { if (purgeWriteBuffers) { int result = mConnection.controlTransfer(FTDI_DEVICE_OUT_REQTYPE, SIO_RESET_REQUEST, - SIO_RESET_PURGE_TX, 0 /* index */, null, 0, USB_WRITE_TIMEOUT_MILLIS); + SIO_RESET_PURGE_TX, mIndex, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Flushing RX failed: result=" + result); } @@ -582,6 +586,9 @@ public class FtdiSerialDriver implements UsbSerialDriver { supportedDevices.put(Integer.valueOf(UsbId.VENDOR_FTDI), new int[] { UsbId.FTDI_FT232R, + UsbId.FTDI_FT232H, + UsbId.FTDI_FT2232H, + UsbId.FTDI_FT4232H, UsbId.FTDI_FT231X, }); return supportedDevices; diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbId.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbId.java index fda7d1f..7bba4db 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbId.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbId.java @@ -33,6 +33,9 @@ public final class UsbId { public static final int VENDOR_FTDI = 0x0403; public static final int FTDI_FT232R = 0x6001; + public static final int FTDI_FT2232H = 0x6010; + public static final int FTDI_FT4232H = 0x6011; + public static final int FTDI_FT232H = 0x6014; public static final int FTDI_FT231X = 0x6015; public static final int VENDOR_ATMEL = 0x03EB;