diff --git a/.idea/misc.xml b/.idea/misc.xml index e0d5b93..047238c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,7 +5,7 @@ diff --git a/README.md b/README.md index 1d37158..5037cc6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Jitpack](https://jitpack.io/v/mik3y/usb-serial-for-android.svg)](https://jitpack.io/#mik3y/usb-serial-for-android) [![Codacy](https://api.codacy.com/project/badge/Grade/4d528e82e35d42d49f659e9b93a9c77d)](https://www.codacy.com/manual/kai-morich/usb-serial-for-android-mik3y?utm_source=github.com&utm_medium=referral&utm_content=mik3y/usb-serial-for-android&utm_campaign=Badge_Grade) +[![codecov](https://codecov.io/gh/mik3y/usb-serial-for-android/branch/master/graph/badge.svg)](https://codecov.io/gh/mik3y/usb-serial-for-android) # usb-serial-for-android diff --git a/build.gradle b/build.gradle index 24bd6cf..1d6c342 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.1' } } diff --git a/usbSerialExamples/build.gradle b/usbSerialExamples/build.gradle index b45091c..5f27979 100644 --- a/usbSerialExamples/build.gradle +++ b/usbSerialExamples/build.gradle @@ -9,6 +9,7 @@ android { targetSdkVersion 28 testInstrumentationRunner "android.test.InstrumentationTestRunner" + missingDimensionStrategy 'device', 'anyDevice' } buildTypes { diff --git a/usbSerialForAndroid/build.gradle b/usbSerialForAndroid/build.gradle index 4961de7..c943135 100644 --- a/usbSerialForAndroid/build.gradle +++ b/usbSerialForAndroid/build.gradle @@ -8,6 +8,11 @@ android { minSdkVersion 17 targetSdkVersion 28 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments = [ + 'rfc2217_server_host': '192.168.0.171', + 'rfc2217_server_nonstandard_baudrates': 'false', // false on Windows, Raspi + 'rfc2217_server_parity_mark_space': 'true' // false on Raspi + ] } buildTypes { @@ -26,7 +31,6 @@ dependencies { androidTestImplementation 'org.apache.commons:commons-lang3:3.8.1' } -// to build a .jar file use gradle task 'createFullJarRelease' - -// to deploy into local maven repository use gradle task 'publishToMavenLocal' and enable: //apply from: 'publishToMavenLocal.gradle' + +//apply from: 'coverage.gradle' diff --git a/usbSerialForAndroid/coverage.gradle b/usbSerialForAndroid/coverage.gradle new file mode 100644 index 0000000..f5c6fa4 --- /dev/null +++ b/usbSerialForAndroid/coverage.gradle @@ -0,0 +1,45 @@ +apply plugin: 'jacoco' + +android { + flavorDimensions 'device' + productFlavors { + anyDevice { + // Used as fallback in usbSerialExample/build.gradle -> missingDimensionStrategy, but not for coverage report + dimension 'device' + } + arduino { + dimension 'device' + testInstrumentationRunnerArguments = ['test_device_driver': 'CdcAcm'] + } + ch340 { + dimension 'device' + testInstrumentationRunnerArguments = ['test_device_driver': 'Ch34x'] + } + cp2102 { // and cp2105 first port + dimension 'device' + testInstrumentationRunnerArguments = ['test_device_driver': 'Cp21xx'] + } + cp2105 { // second port + dimension 'device' + testInstrumentationRunnerArguments = ['test_device_driver': 'Cp21xx', 'test_device_port': '1'] + } + ft232 { // and ft2232 first port + dimension 'device' + testInstrumentationRunnerArguments = ['test_device_driver': 'Ftdi'] + } + ft2232 { // second port + dimension 'device' + testInstrumentationRunnerArguments = ['test_device_driver': 'Ftdi', 'test_device_port': '1'] + } + pl2302 { + dimension 'device' + testInstrumentationRunnerArguments = ['test_device_driver': 'Prolific'] + } + } + + buildTypes { + debug { + testCoverageEnabled true + } + } +} 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 e20ab5e..992d46a 100644 --- a/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java +++ b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java @@ -1,23 +1,8 @@ /* - * 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 ...) @@ -33,10 +18,10 @@ import android.content.IntentFilter; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbManager; +import android.os.Process; 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; @@ -79,12 +64,13 @@ 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; + // testInstrumentationRunnerArguments configuration + private static String rfc2217_server_host; + private static int rfc2217_server_port = 2217; + private static boolean rfc2217_server_nonstandard_baudrates; + private static boolean rfc2217_server_parity_mark_space; + private static String test_device_driver; + private static int test_device_port; private final static int TELNET_READ_WAIT = 500; private final static int USB_READ_WAIT = 500; @@ -116,9 +102,15 @@ public class DeviceTest implements SerialInputOutputManager.Listener { @BeforeClass public static void setUpFixture() throws Exception { + rfc2217_server_host = InstrumentationRegistry.getArguments().getString("rfc2217_server_host"); + rfc2217_server_nonstandard_baudrates = Boolean.valueOf(InstrumentationRegistry.getArguments().getString("rfc2217_server_nonstandard_baudrates")); + rfc2217_server_parity_mark_space = Boolean.valueOf(InstrumentationRegistry.getArguments().getString("rfc2217_server_parity_mark_space")); + test_device_driver = InstrumentationRegistry.getArguments().getString("test_device_driver"); + test_device_port = Integer.valueOf(InstrumentationRegistry.getArguments().getString("test_device_port","0")); + + // postpone parts of fixture setup to first test, because exceptions are not reported for @BeforeClass + // and test terminates with misleading 'Empty test suite' 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 { @@ -150,11 +142,15 @@ public class DeviceTest implements SerialInputOutputManager.Listener { 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()); + assertEquals("no USB device found", 1, availableDrivers.size()); usbSerialDriver = availableDrivers.get(0); + if(test_device_driver != null) { + String driverName = usbSerialDriver.getClass().getSimpleName(); + assertEquals(test_device_driver+"SerialDriver", driverName); + } assertTrue( usbSerialDriver.getPorts().size() > test_device_port); usbSerialPort = usbSerialDriver.getPorts().get(test_device_port); - Log.i(TAG, "Using USB device "+ usbSerialDriver.getClass().getSimpleName()); + Log.i(TAG, "Using USB device "+ usbSerialPort.toString()+" driver="+usbSerialDriver.getClass().getSimpleName()); isCp21xxRestrictedPort = usbSerialDriver instanceof Cp21xxSerialDriver && usbSerialDriver.getPorts().size()==2 && test_device_port == 1; if (!usbManager.hasPermission(usbSerialPort.getDriver().getDevice())) { @@ -372,7 +368,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { @Override public void onRunError(Exception e) { - assertTrue("usb connection lost", false); + fail("usb connection lost"); } // clone of org.apache.commons.lang3.StringUtils.indexOfDifference + optional startpos @@ -401,6 +397,27 @@ public class DeviceTest implements SerialInputOutputManager.Listener { return -1; } + private void logDifference(final StringBuilder data, final StringBuilder expected) { + int datapos = indexOfDifference(data, expected); + int expectedpos = datapos; + while(datapos != -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; + } + } + Log.i(TAG, "difference at " + datapos + " len " + len ); + Log.d(TAG, " got " + data.substring(Math.max(datapos - 20, 0), Math.min(datapos + 20, data.length()))); + Log.d(TAG, " expected " + expected.substring(Math.max(expectedpos - 20, 0), Math.min(expectedpos + 20, expected.length()))); + datapos = indexOfDifference(data, expected, nextdatapos, nextexpectedpos); + expectedpos = nextexpectedpos + (datapos - nextdatapos); + } + } @Test public void baudRate() throws Exception { @@ -435,8 +452,8 @@ 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) { + } catch (java.io.IOException ignored) { // cp2105 second port + } catch (java.lang.IllegalArgumentException ignored) { } try { usbParameters(0, 8, 1, UsbSerialPort.PARITY_NONE); @@ -448,9 +465,9 @@ public class DeviceTest implements SerialInputOutputManager.Listener { ; // todo: add range check in driver else fail("invalid baudrate 0"); - } catch (java.lang.ArithmeticException e) { // ch340 - } catch (java.io.IOException e) { // cp2105 second port - } catch (java.lang.IllegalArgumentException e) { + } catch (java.lang.ArithmeticException ignored) { // ch340 + } catch (java.io.IOException ignored) { // cp2105 second port + } catch (java.lang.IllegalArgumentException ignored) { } try { usbParameters(1, 8, 1, UsbSerialPort.PARITY_NONE); @@ -464,8 +481,8 @@ public class DeviceTest implements SerialInputOutputManager.Listener { ; else fail("invalid baudrate 0"); - } catch (java.io.IOException e) { // ch340 - } catch (java.lang.IllegalArgumentException e) { + } catch (java.io.IOException ignored) { // ch340 + } catch (java.lang.IllegalArgumentException ignored) { } try { usbParameters(2<<31, 8, 1, UsbSerialPort.PARITY_NONE); @@ -477,9 +494,9 @@ 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) { + } catch (java.lang.ArithmeticException ignored) { // ch340 + } catch (java.io.IOException ignored) { // cp2105 second port + } catch (java.lang.IllegalArgumentException ignored) { } for(int baudRate : new int[] {300, 2400, 19200, 42000, 115200} ) { @@ -488,8 +505,8 @@ public class DeviceTest implements SerialInputOutputManager.Listener { if(baudRate == 300 && isCp21xxRestrictedPort) { try { usbParameters(baudRate, 8, 1, UsbSerialPort.PARITY_NONE); - assertTrue(false); - } catch (java.io.IOException e) { + fail("baudrate 300 on cp21xx restricted port"); + } catch (java.io.IOException ignored) { } continue; } @@ -498,10 +515,10 @@ public class DeviceTest implements SerialInputOutputManager.Listener { telnetWrite("net2usb".getBytes()); data = usbRead(7); - assertThat(String.valueOf(baudRate)+"/8N1", data, equalTo("net2usb".getBytes())); + assertThat(baudRate+"/8N1", data, equalTo("net2usb".getBytes())); usbWrite("usb2net".getBytes()); data = telnetRead(7); - assertThat(String.valueOf(baudRate)+"/8N1", data, equalTo("usb2net".getBytes())); + assertThat(baudRate+"/8N1", data, equalTo("usb2net".getBytes())); } { // non matching baud rate telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); @@ -529,7 +546,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { ; // todo: add range check in driver else fail("invalid databits "+i); - } catch (java.lang.IllegalArgumentException e) { + } catch (java.lang.IllegalArgumentException ignored) { } } @@ -607,7 +624,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { try { usbParameters(19200, 8, 1, i); fail("invalid parity "+i); - } catch (java.lang.IllegalArgumentException e) { + } catch (java.lang.IllegalArgumentException ignored) { } } if(isCp21xxRestrictedPort) { @@ -616,9 +633,12 @@ public class DeviceTest implements SerialInputOutputManager.Listener { usbParameters(19200, 8, 1, UsbSerialPort.PARITY_ODD); try { usbParameters(19200, 8, 1, UsbSerialPort.PARITY_MARK); + fail("parity mark"); + } catch (java.lang.IllegalArgumentException ignored) {} + try { usbParameters(19200, 8, 1, UsbSerialPort.PARITY_SPACE); - } catch (java.lang.IllegalArgumentException e) { - } + fail("parity space"); + } catch (java.lang.IllegalArgumentException ignored) {} return; // test below not possible as it requires unsupported 7 dataBits } @@ -701,12 +721,13 @@ public class DeviceTest implements SerialInputOutputManager.Listener { try { usbParameters(19200, 8, i, UsbSerialPort.PARITY_NONE); fail("invalid stopbits " + i); - } catch (java.lang.IllegalArgumentException e) { + } catch (java.lang.IllegalArgumentException ignored) { } } if (usbSerialDriver instanceof CdcAcmSerialDriver) { - // software based bridge in arduino_leonardo_bridge.ino is to slow, other devices might support it + usbParameters(19200, 8, UsbSerialPort.STOPBITS_2, UsbSerialPort.PARITY_NONE); + // software based bridge in arduino_leonardo_bridge.ino is to slow for real test, other devices might support it } else { // shift stopbits into next byte, by using different databits // a - start bit (0) @@ -764,12 +785,12 @@ public class DeviceTest implements SerialInputOutputManager.Listener { 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()); + assertEquals(availableDrivers.get(0).getClass(), usbSerialDriver.getClass()); } @Test - // data loss es expected, if data is not consumed fast enough - public void readBuffer() throws Exception { + // provoke data loss, when data is not read fast enough + public void readBufferOverflow() 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); @@ -778,10 +799,10 @@ public class DeviceTest implements SerialInputOutputManager.Listener { StringBuilder expected = new StringBuilder(); StringBuilder data = new StringBuilder(); final int maxWait = 2000; - int bufferSize = 0; + int bufferSize; for(bufferSize = 8; bufferSize < (2<<15); bufferSize *= 2) { - int linenr = 0; - String line; + int linenr; + String line="-"; expected.setLength(0); data.setLength(0); @@ -794,7 +815,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { } usbReadBlock = false; - // slowly write new data, until old data is comletely read from buffer and new data is received again + // slowly write new data, until old data is completely read from buffer and new data is received boolean found = false; for (; linenr < bufferSize/8 + maxWait/10 && !found; linenr++) { line = String.format("%07d,", linenr); @@ -804,47 +825,38 @@ public class DeviceTest implements SerialInputOutputManager.Listener { data.append(new String(usbRead(0))); found = data.toString().endsWith(line); } - if(!found) { + while(!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"); + byte[] rest = usbRead(-1); + if(rest.length == 0) + fail("last line "+line+" not found"); + data.append(new String(rest)); + found = data.toString().endsWith(line); } if (data.length() != expected.length()) break; } - 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 - // bufferSize 64, first difference at 57 + + logDifference(data, expected); assertTrue(bufferSize > 16); 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 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 + // + // readBufferOverflow provokes read errors, but they can also happen here where the data is actually read fast enough. + // 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. + // Using SERIAL_INPUT_OUTPUT_MANAGER_THREAD_PRIORITY=THREAD_PRIORITY_URGENT_AUDIO sometimes reduced errors by factor 10, sometimes not at all! + // int baudrate = 115200; usbParameters(baudrate, 8, 1, UsbSerialPort.PARITY_NONE); telnetParameters(baudrate, 8, 1, UsbSerialPort.PARITY_NONE); - // 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) @@ -874,45 +886,17 @@ public class DeviceTest implements SerialInputOutputManager.Listener { dlen = data.length(); elen = expected.length(); } - boolean found = false; - 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))); - found = data.toString().endsWith(line); - Thread.sleep(1); - } - //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); + boolean found = false; + while(!found) { + // use waiting read to clear input queue, else next test would see unexpected data + byte[] rest = usbRead(-1); + if(rest.length == 0) + break; + data.append(new String(rest)); + found = data.toString().endsWith(line); } - 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); + logDifference(data, expected); } @Test