From 73b8b73133c4fa2fc299824aa52082efc8bd6c32 Mon Sep 17 00:00:00 2001 From: Guillaume Revaillot Date: Wed, 16 Nov 2016 13:58:01 +0100 Subject: [PATCH 01/20] ch34xSerialDriver: support more baudrates. allow to use 57600bps with ch34x based adapter by using dynamic computation of baudrate configuration, based on linux kernel's driver. --- .../usbserial/driver/Ch34xSerialDriver.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) 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 1155512..dc03c7a 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 @@ -271,30 +271,34 @@ public class Ch34xSerialDriver implements UsbSerialDriver { private void setBaudRate(int baudRate) throws IOException { - int[] baud = new int[]{2400, 0xd901, 0x0038, 4800, 0x6402, - 0x001f, 9600, 0xb202, 0x0013, 19200, 0xd902, 0x000d, 38400, - 0x6403, 0x000a, 115200, 0xcc03, 0x0008}; + final long CH341_BAUDBASE_FACTOR = 1532620800; + final int CH341_BAUDBASE_DIVMAX = 3; - for (int i = 0; i < baud.length / 3; i++) { - if (baud[i * 3] == baudRate) { - int ret = controlOut(0x9a, 0x1312, baud[i * 3 + 1]); - if (ret < 0) { - throw new IOException("Error setting baud rate. #1"); - } - ret = controlOut(0x9a, 0x0f2c, baud[i * 3 + 2]); - if (ret < 0) { - throw new IOException("Error setting baud rate. #1"); + long factor = CH341_BAUDBASE_FACTOR / baudRate; + int divisor = CH341_BAUDBASE_DIVMAX; + + while ((factor > 0xfff0) && divisor > 0) { + factor >>= 3; + divisor--; } - return; - } + if (factor > 0xfff0) { + throw new IOException("Baudrate " + baudRate + " not supported"); } + factor = 0x10000 - factor; - throw new IOException("Baud rate " + baudRate + " currently not supported"); + int ret = controlOut(0x9a, 0x1312, (int) ((factor & 0xff00) | divisor)); + if (ret < 0) { + throw new IOException("Error setting baud rate. #1)"); + } + + ret = controlOut(0x9a, 0x0f2c, (int) (factor & 0xff)); + if (ret < 0) { + throw new IOException("Error setting baud rate. #2"); + } } - @Override public void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException { From 608c67499a10ccf5c25811e9d1611187192aaa45 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Fri, 17 Feb 2017 22:03:32 +0100 Subject: [PATCH 02/20] support USB devices with other non CDC related endpoints, e.g. when using ARM mbed DAPLink firmware --- .../usbserial/driver/CdcAcmSerialDriver.java | 66 ++++++++++++++----- .../hoho/android/usbserial/driver/UsbId.java | 4 ++ 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java index 4fd84bd..3f4b96d 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java @@ -77,6 +77,8 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { private UsbEndpoint mReadEndpoint; private UsbEndpoint mWriteEndpoint; + private int mControlIndex; + private boolean mRts = false; private boolean mDtr = false; @@ -139,6 +141,7 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { // the following code is inspired by the cdc-acm driver // in the linux kernel + mControlIndex = 0; mControlInterface = mDevice.getInterface(0); Log.d(TAG, "Control iface=" + mControlInterface); @@ -196,34 +199,63 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { private void openInterface() throws IOException { Log.d(TAG, "claiming interfaces, count=" + mDevice.getInterfaceCount()); - mControlInterface = mDevice.getInterface(0); + mControlInterface = null; + mDataInterface = null; + for (int i = 0; i < mDevice.getInterfaceCount(); i++) { + UsbInterface usbInterface = mDevice.getInterface(i); + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_COMM) { + mControlIndex = i; + mControlInterface = usbInterface; + } + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_CDC_DATA) { + mDataInterface = usbInterface; + } + } + + if(mControlInterface == null) { + throw new IOException("no control interface."); + } Log.d(TAG, "Control iface=" + mControlInterface); - // class should be USB_CLASS_COMM if (!mConnection.claimInterface(mControlInterface, true)) { throw new IOException("Could not claim control interface."); } mControlEndpoint = mControlInterface.getEndpoint(0); - Log.d(TAG, "Control endpoint direction: " + mControlEndpoint.getDirection()); + if (mControlEndpoint.getDirection() != UsbConstants.USB_DIR_IN || mControlEndpoint.getType() != UsbConstants.USB_ENDPOINT_XFER_INT) { + throw new IOException("invalid control endpoint"); + } - Log.d(TAG, "Claiming data interface."); - mDataInterface = mDevice.getInterface(1); + if(mDataInterface == null) { + throw new IOException("no data interface."); + } Log.d(TAG, "data iface=" + mDataInterface); - // class should be USB_CLASS_CDC_DATA if (!mConnection.claimInterface(mDataInterface, true)) { throw new IOException("Could not claim data interface."); } - mReadEndpoint = mDataInterface.getEndpoint(1); - Log.d(TAG, "Read endpoint direction: " + mReadEndpoint.getDirection()); - mWriteEndpoint = mDataInterface.getEndpoint(0); - Log.d(TAG, "Write endpoint direction: " + mWriteEndpoint.getDirection()); + + mReadEndpoint = null; + mWriteEndpoint = null; + for (int i = 0; i < mDataInterface.getEndpointCount(); i++) { + UsbEndpoint ep = mDataInterface.getEndpoint(i); + if (ep.getDirection() == UsbConstants.USB_DIR_IN && ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) + mReadEndpoint = ep; + if (ep.getDirection() == UsbConstants.USB_DIR_OUT && ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) + mWriteEndpoint = ep; + } + if (mReadEndpoint == null || mWriteEndpoint == null) { + throw new IOException("Could not get read&write endpoints."); + } } - private int sendAcmControlMessage(int request, int value, byte[] buf) { - return mConnection.controlTransfer( - USB_RT_ACM, request, value, 0, buf, buf != null ? buf.length : 0, 5000); + private int sendAcmControlMessage(int request, int value, byte[] buf) throws IOException { + int len = mConnection.controlTransfer( + USB_RT_ACM, request, value, mControlIndex, buf, buf != null ? buf.length : 0, 5000); + if(len < 0) { + throw new IOException("controlTransfer failed."); + } + return len; } @Override @@ -320,7 +352,7 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { } @Override - public void setParameters(int baudRate, int dataBits, int stopBits, int parity) { + public void setParameters(int baudRate, int dataBits, int stopBits, int parity) throws IOException { byte stopBitsByte; switch (stopBits) { case STOPBITS_1: stopBitsByte = 0; break; @@ -392,7 +424,7 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { setDtrRts(); } - private void setDtrRts() { + private void setDtrRts() throws IOException { int value = (mRts ? 0x2 : 0) | (mDtr ? 0x1 : 0); sendAcmControlMessage(SET_CONTROL_LINE_STATE, value, null); } @@ -426,6 +458,10 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { new int[] { UsbId.LEAFLABS_MAPLE, }); + supportedDevices.put(Integer.valueOf(UsbId.VENDOR_ARM), + new int[] { + UsbId.ARM_MBED, + }); 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 c4050e1..fda7d1f 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 @@ -68,6 +68,10 @@ public final class UsbId { public static final int VENDOR_QINHENG = 0x1a86; public static final int QINHENG_HL340 = 0x7523; + // at www.linux-usb.org/usb.ids listed for NXP/LPC1768, but all processors supported by ARM mbed DAPLink firmware report these ids + public static final int VENDOR_ARM = 0x0d28; + public static final int ARM_MBED = 0x0204; + private UsbId() { throw new IllegalAccessError("Non-instantiable class."); } From fcd8596bdd3c267731fc40b19818757399c73241 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sat, 1 Apr 2017 23:15:55 +0200 Subject: [PATCH 03/20] enable async read for FTDI as in CDC driver. this prevents -1 error from bulkTransfer() when receiving data in multiple packets, e.g. if consumed to slow at high baud rates. prevent loss of last packet if full --- .../usbserial/driver/FtdiSerialDriver.java | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) 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 1a9a66b..708710e 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 @@ -26,6 +26,7 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbRequest; +import android.os.Build; import android.util.Log; import com.hoho.android.usbserial.util.HexDump; @@ -190,10 +191,11 @@ public class FtdiSerialDriver implements UsbSerialDriver { * since it gives no indication of number of bytes read. Set this to * {@code true} on platforms where it is fixed. */ - private static final boolean ENABLE_ASYNC_READS = false; + private final boolean mEnableAsyncReads; public FtdiSerialPort(UsbDevice device, int portNumber) { super(device, portNumber); + mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1); } @Override @@ -210,10 +212,10 @@ public class FtdiSerialDriver implements UsbSerialDriver { * @return The number of payload bytes */ private final int filterStatusBytes(byte[] src, byte[] dest, int totalBytesRead, int maxPacketSize) { - final int packetsCount = totalBytesRead / maxPacketSize + (totalBytesRead % maxPacketSize == 0 ? 0 : 1); + final int packetsCount = (totalBytesRead + maxPacketSize -1 )/ maxPacketSize; for (int packetIdx = 0; packetIdx < packetsCount; ++packetIdx) { final int count = (packetIdx == (packetsCount - 1)) - ? (totalBytesRead % maxPacketSize) - MODEM_STATUS_HEADER_LENGTH + ? totalBytesRead - packetIdx * maxPacketSize - MODEM_STATUS_HEADER_LENGTH : maxPacketSize - MODEM_STATUS_HEADER_LENGTH; if (count > 0) { System.arraycopy(src, @@ -280,33 +282,30 @@ public class FtdiSerialDriver implements UsbSerialDriver { public int read(byte[] dest, int timeoutMillis) throws IOException { final UsbEndpoint endpoint = mDevice.getInterface(0).getEndpoint(0); - if (ENABLE_ASYNC_READS) { - final int readAmt; - synchronized (mReadBufferLock) { - // mReadBuffer is only used for maximum read size. - readAmt = Math.min(dest.length, mReadBuffer.length); - } - + if (mEnableAsyncReads) { final UsbRequest request = new UsbRequest(); - request.initialize(mConnection, endpoint); - final ByteBuffer buf = ByteBuffer.wrap(dest); - if (!request.queue(buf, readAmt)) { - throw new IOException("Error queueing request."); + try { + request.initialize(mConnection, endpoint); + 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"); + } + } finally { + request.close(); } - final UsbRequest response = mConnection.requestWait(); - if (response == null) { - throw new IOException("Null response"); + final int totalBytesRead = buf.position(); + if (totalBytesRead < MODEM_STATUS_HEADER_LENGTH) { + throw new IOException("Expected at least " + MODEM_STATUS_HEADER_LENGTH + " bytes"); } - final int payloadBytesRead = buf.position() - MODEM_STATUS_HEADER_LENGTH; - if (payloadBytesRead > 0) { - Log.d(TAG, HexDump.dumpHexString(dest, 0, Math.min(32, dest.length))); - return payloadBytesRead; - } else { - return 0; - } + return filterStatusBytes(dest, dest, totalBytesRead, endpoint.getMaxPacketSize()); + } else { final int totalBytesRead; From df4e9174cb005befcf4cbcec801f4f2452af641f Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Fri, 3 Nov 2017 22:52:32 +0100 Subject: [PATCH 04/20] enable async read for CH340 as in CDC driver to prevent data loss at high baud rates --- .../usbserial/driver/Ch34xSerialDriver.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) 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 1155512..a2fd52e 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 @@ -25,9 +25,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; @@ -70,11 +73,13 @@ public class Ch34xSerialDriver implements UsbSerialDriver { private boolean dtr = false; private boolean rts = false; + private final boolean mEnableAsyncReads; private UsbEndpoint mReadEndpoint; private UsbEndpoint mWriteEndpoint; public Ch340SerialPort(UsbDevice device, int portNumber) { super(device, portNumber); + mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1); } @Override @@ -112,6 +117,11 @@ public class Ch34xSerialDriver implements UsbSerialDriver { } } + if (mEnableAsyncReads) { + Log.d(TAG, "Async reads enabled"); + } else { + Log.d(TAG, "Async reads disabled."); + } initialize(); setBaudRate(DEFAULT_BAUD_RATE); @@ -146,6 +156,32 @@ public class Ch34xSerialDriver implements UsbSerialDriver { @Override public int read(byte[] dest, int timeoutMillis) throws IOException { + 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(); + } + } + final int numBytesRead; synchronized (mReadBufferLock) { int readAmt = Math.min(dest.length, mReadBuffer.length); From 9c1ca288aeffd9bc9de3c07a546fde25e5a5b9de Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sat, 20 Jan 2018 13:09:29 +0100 Subject: [PATCH 05/20] CH34x: data bits, parity, stop bits CP21xx: mark+space all devices: return error on unsupported parameters --- .../usbserial/driver/Ch34xSerialDriver.java | 68 ++++++++++++++++++- .../usbserial/driver/Cp21xxSerialDriver.java | 18 ++++- .../usbserial/driver/FtdiSerialDriver.java | 16 ++++- 3 files changed, 94 insertions(+), 8 deletions(-) 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 1155512..bedd8f8 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 @@ -46,6 +46,17 @@ public class Ch34xSerialDriver implements UsbSerialDriver { private final UsbDevice mDevice; private final UsbSerialPort mPort; + private static final int LCR_ENABLE_RX = 0x80; + private static final int LCR_ENABLE_TX = 0x40; + private static final int LCR_MARK_SPACE = 0x20; + private static final int LCR_PAR_EVEN = 0x10; + private static final int LCR_ENABLE_PAR = 0x08; + private static final int LCR_STOP_BITS_2 = 0x04; + private static final int LCR_CS8 = 0x03; + private static final int LCR_CS7 = 0x02; + private static final int LCR_CS6 = 0x01; + private static final int LCR_CS5 = 0x00; + public Ch34xSerialDriver(UsbDevice device) { mDevice = device; mPort = new Ch340SerialPort(mDevice, 0); @@ -252,7 +263,7 @@ public class Ch34xSerialDriver implements UsbSerialDriver { checkState("init #4", 0x95, 0x2518, new int[]{-1 /* 0x56, c3*/, 0x00}); - if (controlOut(0x9a, 0x2518, 0x0050) < 0) { + if (controlOut(0x9a, 0x2518, LCR_ENABLE_RX | LCR_ENABLE_TX | LCR_CS8) < 0) { throw new IOException("init failed! #5"); } @@ -300,7 +311,60 @@ public class Ch34xSerialDriver implements UsbSerialDriver { throws IOException { setBaudRate(baudRate); - // TODO databit, stopbit and paraty set not implemented + int lcr = LCR_ENABLE_RX | LCR_ENABLE_TX; + + switch (dataBits) { + case DATABITS_5: + lcr |= LCR_CS5; + break; + case DATABITS_6: + lcr |= LCR_CS6; + break; + case DATABITS_7: + lcr |= LCR_CS7; + break; + case DATABITS_8: + lcr |= LCR_CS8; + break; + default: + throw new IllegalArgumentException("Unknown dataBits value: " + dataBits); + } + + switch (parity) { + case PARITY_NONE: + break; + case PARITY_ODD: + lcr |= LCR_ENABLE_PAR; + break; + case PARITY_EVEN: + lcr |= LCR_ENABLE_PAR | LCR_PAR_EVEN; + break; + case PARITY_MARK: + lcr |= LCR_ENABLE_PAR | LCR_MARK_SPACE; + break; + case PARITY_SPACE: + lcr |= LCR_ENABLE_PAR | LCR_MARK_SPACE | LCR_PAR_EVEN; + break; + default: + throw new IllegalArgumentException("Unknown parity value: " + parity); + } + + switch (stopBits) { + case STOPBITS_1: + break; + case STOPBITS_1_5: + throw new IllegalArgumentException("Unsupported stopBits value: 1.5"); + case STOPBITS_2: + lcr |= LCR_STOP_BITS_2; + break; + default: + throw new IllegalArgumentException("Unknown stopBits value: " + stopBits); + } + + int ret = controlOut(0x9a, 0x2518, lcr); + if (ret < 0) { + throw new IOException("Error setting control byte"); + } } @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 1faf5df..b5956a8 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 @@ -264,26 +264,38 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { configDataBits |= 0x0800; break; default: - configDataBits |= 0x0800; - break; + throw new IllegalArgumentException("Unknown dataBits value: " + dataBits); } switch (parity) { + case PARITY_NONE: + break; case PARITY_ODD: configDataBits |= 0x0010; break; case PARITY_EVEN: configDataBits |= 0x0020; break; + case PARITY_MARK: + configDataBits |= 0x0030; + break; + case PARITY_SPACE: + configDataBits |= 0x0040; + break; + default: + throw new IllegalArgumentException("Unknown parity value: " + parity); } switch (stopBits) { case STOPBITS_1: - configDataBits |= 0; break; + case STOPBITS_1_5: + throw new IllegalArgumentException("Unsupported stopBits value: 1.5"); case STOPBITS_2: configDataBits |= 2; break; + default: + throw new IllegalArgumentException("Unknown stopBits value: " + stopBits); } setConfigSingle(SILABSER_SET_LINE_CTL_REQUEST_CODE, configDataBits); } 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 1a9a66b..ff003f0 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 @@ -379,7 +379,18 @@ public class FtdiSerialDriver implements UsbSerialDriver { throws IOException { setBaudRate(baudRate); - int config = dataBits; + int config = 0; + switch (dataBits) { + case DATABITS_5: + case DATABITS_6: + throw new IllegalArgumentException("Unsupported dataBits value: " + dataBits); + case DATABITS_7: + case DATABITS_8: + config |= dataBits; + break; + default: + throw new IllegalArgumentException("Unknown dataBits value: " + dataBits); + } switch (parity) { case PARITY_NONE: @@ -406,8 +417,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { config |= (0x00 << 11); break; case STOPBITS_1_5: - config |= (0x01 << 11); - break; + throw new IllegalArgumentException("Unsupported stopBits value: 1.5"); case STOPBITS_2: config |= (0x02 << 11); break; From adb22f718e03ee36a8977b4a517d05d74d748795 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Fri, 9 Mar 2018 22:37:06 +0100 Subject: [PATCH 06/20] 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 07/20] 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 08/20] 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; From e527afdf3550994a61dbf5b91fba61f4d5431294 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sun, 11 Nov 2018 09:29:09 +0100 Subject: [PATCH 09/20] compile+target sdk 28, gradle 4.6 --- .idea/misc.xml | 10 +++++++--- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- usbSerialExamples/build.gradle | 6 +++--- usbSerialExamples/src/main/AndroidManifest.xml | 2 -- .../android/usbserial/examples/DeviceListActivity.java | 0 .../usbserial/examples/SerialConsoleActivity.java | 0 usbSerialForAndroid/build.gradle | 10 +++++----- usbSerialForAndroid/src/main/AndroidManifest.xml | 1 - 9 files changed, 18 insertions(+), 17 deletions(-) rename usbSerialExamples/src/main/java/{src => }/com/hoho/android/usbserial/examples/DeviceListActivity.java (100%) rename usbSerialExamples/src/main/java/{src => }/com/hoho/android/usbserial/examples/SerialConsoleActivity.java (100%) diff --git a/.idea/misc.xml b/.idea/misc.xml index c0f68ed..e0d5b93 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,27 +5,31 @@ - + diff --git a/build.gradle b/build.gradle index df159ef..f400f0c 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.2' + classpath 'com.android.tools.build:gradle:3.2.1' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eb10a04..93a408c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Mar 27 21:28:01 CEST 2018 +#Sun Nov 11 09:16:07 CET 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip diff --git a/usbSerialExamples/build.gradle b/usbSerialExamples/build.gradle index cde7b10..fbcfcfa 100644 --- a/usbSerialExamples/build.gradle +++ b/usbSerialExamples/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 27 - buildToolsVersion '27.0.3' + compileSdkVersion 28 + buildToolsVersion '28.0.3' defaultConfig { minSdkVersion 14 - targetSdkVersion 27 + targetSdkVersion 28 testInstrumentationRunner "android.test.InstrumentationTestRunner" } diff --git a/usbSerialExamples/src/main/AndroidManifest.xml b/usbSerialExamples/src/main/AndroidManifest.xml index 26ca72c..efa205f 100644 --- a/usbSerialExamples/src/main/AndroidManifest.xml +++ b/usbSerialExamples/src/main/AndroidManifest.xml @@ -4,8 +4,6 @@ android:versionCode="1" android:versionName="1.0" > - - - From 0d48ed04e7cf3ff83c8089d4dedf61f1ba7277be Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sun, 3 Feb 2019 10:22:53 +0100 Subject: [PATCH 10/20] Always use async read, as bulkTransfer can cause data loss. Increase API version to 17 because async read only works reliably since Android 4.2 (http://b.android.com/28023) --- README.md | 2 +- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- usbSerialExamples/build.gradle | 2 +- usbSerialForAndroid/build.gradle | 2 +- .../usbserial/driver/CdcAcmSerialDriver.java | 48 ++---------- .../usbserial/driver/Ch34xSerialDriver.java | 77 ++++++------------- .../usbserial/driver/Cp21xxSerialDriver.java | 72 +++++++---------- .../usbserial/driver/FtdiSerialDriver.java | 63 +++++---------- .../driver/ProlificSerialDriver.java | 56 +++++--------- 10 files changed, 103 insertions(+), 225 deletions(-) diff --git a/README.md b/README.md index 50a9551..e2fb7b2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This is a driver library for communication with Arduinos and other USB serial hardware on Android, using the [Android USB Host API](http://developer.android.com/guide/topics/connectivity/usb/host.html) -available on Android 3.1+. +available since Android 3.1 and asynchronous interrupt transfer working reliably since Android 4.2 No root access, ADK, or special kernel drivers are required; all drivers are implemented in Java. You get a raw serial port with `read()`, `write()`, and other basic diff --git a/build.gradle b/build.gradle index f400f0c..f055c90 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:3.3.0' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 93a408c..b2a4404 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Nov 11 09:16:07 CET 2018 +#Sun Feb 03 09:37:03 CET 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/usbSerialExamples/build.gradle b/usbSerialExamples/build.gradle index fbcfcfa..b45091c 100644 --- a/usbSerialExamples/build.gradle +++ b/usbSerialExamples/build.gradle @@ -5,7 +5,7 @@ android { buildToolsVersion '28.0.3' defaultConfig { - minSdkVersion 14 + minSdkVersion 17 targetSdkVersion 28 testInstrumentationRunner "android.test.InstrumentationTestRunner" diff --git a/usbSerialForAndroid/build.gradle b/usbSerialForAndroid/build.gradle index 0e66b22..bbb2b57 100644 --- a/usbSerialForAndroid/build.gradle +++ b/usbSerialForAndroid/build.gradle @@ -7,7 +7,7 @@ android { buildToolsVersion '28.0.3' defaultConfig { - minSdkVersion 14 + minSdkVersion 17 targetSdkVersion 28 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java index 3f4b96d..a3d9a76 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java @@ -27,7 +27,6 @@ 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; @@ -69,7 +68,6 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { class CdcAcmSerialPort extends CommonUsbSerialPort { - private final boolean mEnableAsyncReads; private UsbInterface mControlInterface; private UsbInterface mDataInterface; @@ -92,7 +90,6 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { public CdcAcmSerialPort(UsbDevice device, int portNumber) { super(device, portNumber); - mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1); } @Override @@ -118,13 +115,6 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { openInterface(); } - if (mEnableAsyncReads) { - Log.d(TAG, "Async reads enabled"); - } else { - Log.d(TAG, "Async reads disabled."); - } - - opened = true; } finally { if (!opened) { @@ -269,51 +259,29 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { @Override public int read(byte[] dest, int timeoutMillis) throws IOException { - if (mEnableAsyncReads) { - final UsbRequest request = new UsbRequest(); - try { + 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."); + throw new IOException("Error queueing request."); } final UsbRequest response = mConnection.requestWait(); if (response == null) { - throw new IOException("Null response"); + 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; + //Log.d(TAG, HexDump.dumpHexString(dest, 0, Math.min(32, dest.length))); + return nread; } else { - return 0; - } - } 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 - if (timeoutMillis == Integer.MAX_VALUE) { - // Hack: Special case "~infinite timeout" as an error. - return -1; - } return 0; } - System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead); + } finally { + request.close(); } - return numBytesRead; } @Override 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 48b26fc..0b0ae97 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 @@ -26,7 +26,6 @@ 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; @@ -84,14 +83,12 @@ public class Ch34xSerialDriver implements UsbSerialDriver { private boolean dtr = false; private boolean rts = false; - private final Boolean mEnableAsyncReads; private UsbEndpoint mReadEndpoint; private UsbEndpoint mWriteEndpoint; private UsbRequest mUsbRequest; public Ch340SerialPort(UsbDevice device, int portNumber) { super(device, portNumber); - mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1); } @Override @@ -127,12 +124,6 @@ public class Ch34xSerialDriver implements UsbSerialDriver { } } - if (mEnableAsyncReads) { - Log.d(TAG, "Async reads enabled"); - } else { - Log.d(TAG, "Async reads disabled."); - } - initialize(); setBaudRate(DEFAULT_BAUD_RATE); @@ -153,12 +144,10 @@ public class Ch34xSerialDriver implements UsbSerialDriver { if (mConnection == null) { throw new IOException("Already closed"); } - synchronized (mEnableAsyncReads) { + synchronized (this) { if (mUsbRequest != null) mUsbRequest.cancel(); } - // TODO: nothing sended on close, maybe needed? - try { mConnection.close(); } finally { @@ -169,50 +158,32 @@ public class Ch34xSerialDriver implements UsbSerialDriver { @Override public int read(byte[] dest, int timeoutMillis) throws IOException { - 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."); - } - mUsbRequest = request; - final UsbRequest response = mConnection.requestWait(); - synchronized (mEnableAsyncReads) { - mUsbRequest = null; - } - 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 { + 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."); + } + mUsbRequest = request; + final UsbRequest response = mConnection.requestWait(); + synchronized (this) { mUsbRequest = null; - request.close(); } - } 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); + if (response == null) { + throw new IOException("Null response"); } - return numBytesRead; + + 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 { + mUsbRequest = null; + request.close(); } } 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 2119130..4bc07ee 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 @@ -27,7 +27,6 @@ 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; @@ -107,7 +106,6 @@ 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; private UsbRequest mUsbRequest; @@ -118,7 +116,6 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { public Cp21xxSerialPort(UsbDevice device, int portNumber) { super(device, portNumber); - mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1); } @Override @@ -180,13 +177,16 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { if (mConnection == null) { throw new IOException("Already closed"); } - synchronized (mEnableAsyncReads) { + synchronized (this) { if(mUsbRequest != null) { mUsbRequest.cancel(); } } try { setConfigSingle(SILABSER_IFC_ENABLE_REQUEST_CODE, UART_DISABLE); + } catch (Exception ignored) + {} + try { mConnection.close(); } finally { mConnection = null; @@ -195,50 +195,32 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { @Override public int read(byte[] dest, int timeoutMillis) throws IOException { - 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."); - } - mUsbRequest = request; - final UsbRequest response = mConnection.requestWait(); - synchronized (mEnableAsyncReads) { - mUsbRequest = null; - } - 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 { + 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."); + } + mUsbRequest = request; + final UsbRequest response = mConnection.requestWait(); + synchronized (this) { mUsbRequest = null; - request.close(); } - } 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); + if (response == null) { + throw new IOException("Null response"); } - return numBytesRead; + + 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 { + mUsbRequest = null; + request.close(); } } 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 757c567..ccdc25e 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 @@ -26,7 +26,6 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbRequest; -import android.os.Build; import android.util.Log; import java.io.IOException; @@ -186,16 +185,8 @@ public class FtdiSerialDriver implements UsbSerialDriver { private int mIndex = 0; - /** - * Due to http://b.android.com/28023 , we cannot use UsbRequest async reads - * since it gives no indication of number of bytes read. Set this to - * {@code true} on platforms where it is fixed. - */ - private final boolean mEnableAsyncReads; - public FtdiSerialPort(UsbDevice device, int portNumber) { super(device, portNumber); - mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1); } @Override @@ -286,46 +277,28 @@ public class FtdiSerialDriver implements UsbSerialDriver { @Override public int read(byte[] dest, int timeoutMillis) throws IOException { final UsbEndpoint endpoint = mDevice.getInterface(mPortNumber).getEndpoint(0); - - if (mEnableAsyncReads) { - final UsbRequest request = new UsbRequest(); - final ByteBuffer buf = ByteBuffer.wrap(dest); - try { - request.initialize(mConnection, endpoint); - 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"); - } - } finally { - request.close(); + final UsbRequest request = new UsbRequest(); + final ByteBuffer buf = ByteBuffer.wrap(dest); + try { + request.initialize(mConnection, endpoint); + if (!request.queue(buf, dest.length)) { + throw new IOException("Error queueing request."); } - final int totalBytesRead = buf.position(); - if (totalBytesRead < MODEM_STATUS_HEADER_LENGTH) { - throw new IOException("Expected at least " + MODEM_STATUS_HEADER_LENGTH + " bytes"); - } - - return filterStatusBytes(dest, dest, totalBytesRead, endpoint.getMaxPacketSize()); - - } else { - final int totalBytesRead; - - synchronized (mReadBufferLock) { - final int readAmt = Math.min(dest.length, mReadBuffer.length); - totalBytesRead = mConnection.bulkTransfer(endpoint, mReadBuffer, - readAmt, timeoutMillis); - - if (totalBytesRead < MODEM_STATUS_HEADER_LENGTH) { - throw new IOException("Expected at least " + MODEM_STATUS_HEADER_LENGTH + " bytes"); - } - - return filterStatusBytes(mReadBuffer, dest, totalBytesRead, endpoint.getMaxPacketSize()); + final UsbRequest response = mConnection.requestWait(); + if (response == null) { + throw new IOException("Null response"); } + } finally { + request.close(); } + + final int totalBytesRead = buf.position(); + if (totalBytesRead < MODEM_STATUS_HEADER_LENGTH) { + throw new IOException("Expected at least " + MODEM_STATUS_HEADER_LENGTH + " bytes"); + } + + return filterStatusBytes(dest, dest, totalBytesRead, endpoint.getMaxPacketSize()); } @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 550350c..c3c8b23 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 @@ -33,7 +33,6 @@ 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; @@ -112,7 +111,6 @@ public class ProlificSerialDriver implements UsbSerialDriver { private int mDeviceType = DEVICE_TYPE_HX; - private final boolean mEnableAsyncReads; private UsbEndpoint mReadEndpoint; private UsbEndpoint mWriteEndpoint; private UsbEndpoint mInterruptEndpoint; @@ -130,7 +128,6 @@ 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 @@ -373,41 +370,28 @@ public class ProlificSerialDriver implements UsbSerialDriver { @Override public int read(byte[] dest, int timeoutMillis) throws IOException { - 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(); + 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."); } - } 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; + + 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(); } } From 508c39e66a0791d1e0864a1eab905bb755a03941 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sun, 6 Oct 2019 10:19:04 +0200 Subject: [PATCH 11/20] README cleanup --- README.md | 284 +++++++++++++++++++++++++++--------------------------- 1 file changed, 141 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index e2fb7b2..9c20f4a 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,141 @@ -# usb-serial-for-android - -This is a driver library for communication with Arduinos and other USB serial hardware on -Android, using the -[Android USB Host API](http://developer.android.com/guide/topics/connectivity/usb/host.html) -available since Android 3.1 and asynchronous interrupt transfer working reliably since Android 4.2 - -No root access, ADK, or special kernel drivers are required; all drivers are implemented in -Java. You get a raw serial port with `read()`, `write()`, and other basic -functions for use with your own protocols. - -* **Homepage**: https://github.com/mik3y/usb-serial-for-android -* **Google group**: http://groups.google.com/group/usb-serial-for-android -* **Latest release**: [v0.1.0](https://github.com/mik3y/usb-serial-for-android/releases) - -## Quick Start - -**1.** [Link your project](https://github.com/mik3y/usb-serial-for-android/wiki/Building-From-Source) to the library. - -**2.** Copy [device_filter.xml](https://github.com/mik3y/usb-serial-for-android/blob/master/usbSerialExamples/src/main/res/xml/device_filter.xml) to your project's `res/xml/` directory. - -**3.** Configure your `AndroidManifest.xml` to notify your app when a device is attached (see [Android USB Host documentation](http://developer.android.com/guide/topics/connectivity/usb/host.html#discovering-d) for help). - -```xml - - - - - - -``` - -**4.** Use it! Example code snippet: - -```java -// Find all available drivers from attached devices. -UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); -List availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager); -if (availableDrivers.isEmpty()) { - return; -} - -// Open a connection to the first available driver. -UsbSerialDriver driver = availableDrivers.get(0); -UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); -if (connection == null) { - // You probably need to call UsbManager.requestPermission(driver.getDevice(), ..) - return; -} - -// Read some data! Most have just one port (port 0). -UsbSerialPort port = driver.getPorts().get(0); -try { - port.open(connection); - port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); - - byte buffer[] = new byte[16]; - int numBytesRead = port.read(buffer, 1000); - Log.d(TAG, "Read " + numBytesRead + " bytes."); -} catch (IOException e) { - // Deal with error. -} finally { - port.close(); -} -``` - -For a more complete example, see the -[UsbSerialExamples project](https://github.com/mik3y/usb-serial-for-android/blob/master/usbSerialExamples) -in git, which is a simple application for reading and showing serial data. - -A [simple Arduino application](https://github.com/mik3y/usb-serial-for-android/blob/master/arduino) -is also available which can be used for testing. - - -## Probing for Unrecognized Devices - -Sometimes you may need to do a little extra work to support devices which -usb-serial-for-android doesn't [yet] know about -- but which you know to be -compatible with one of the built-in drivers. This may be the case for a brand -new device or for one using a custom VID/PID pair. - -UsbSerialProber is a class to help you find and instantiate compatible -UsbSerialDrivers from the tree of connected UsbDevices. Normally, you will use -the default prober returned by ``UsbSerialProber.getDefaultProber()``, which -uses the built-in list of well-known VIDs and PIDs that are supported by our -drivers. - -To use your own set of rules, create and use a custom prober: - -```java -// Probe for our custom CDC devices, which use VID 0x1234 -// and PIDS 0x0001 and 0x0002. -ProbeTable customTable = new ProbeTable(); -customTable.addProduct(0x1234, 0x0001, CdcAcmSerialDriver.class); -customTable.addProduct(0x1234, 0x0002, CdcAcmSerialDriver.class); - -UsbSerialProber prober = new UsbSerialProber(customTable); -List drivers = prober.findAllDrivers(usbManager); -// ... -``` - -Of course, nothing requires you to use UsbSerialProber at all: you can -instantiate driver classes directly if you know what you're doing; just supply -a compatible UsbDevice. - - -## Compatible Devices - -* *Serial chips:* FT232R, CDC/ACM (eg Arduino Uno) and possibly others. - See [CompatibleSerialDevices](https://github.com/mik3y/usb-serial-for-android/wiki/Compatible-Serial-Devices). -* *Android phones and tablets:* Nexus 7, Motorola Xoom, and many others. - See [CompatibleAndroidDevices](https://github.com/mik3y/usb-serial-for-android/wiki/Compatible-Android-Devices). - - -## Author, License, and Copyright - -usb-serial-for-android is written and maintained by *mike wakerly*. - -This library is licensed under *LGPL Version 2.1*. Please see LICENSE.txt for the -complete license. - -Copyright 2011-2012, Google Inc. All Rights Reserved. - -Portions of this library are based on libftdi -(http://www.intra2net.com/en/developer/libftdi). Please see -FtdiSerialDriver.java for more information. - -## Help & Discussion - -For common problems, see the -[Troubleshooting](https://github.com/mik3y/usb-serial-for-android/wiki/Troubleshooting) -wiki page. - -For other help and discussion, please join our Google Group, -[usb-serial-for-android](https://groups.google.com/forum/?fromgroups#!forum/usb-serial-for-android). - -Are you using the library? Let us know on the group and we'll add your project to -[ProjectsUsingUsbSerialForAndroid](https://github.com/mik3y/usb-serial-for-android/wiki/Projects-Using-usb-serial-for-android). - +# usb-serial-for-android + +This is a driver library for communication with Arduinos and other USB serial hardware on +Android, using the +[Android USB Host Mode (OTG)](http://developer.android.com/guide/topics/connectivity/usb/host.html) +available since Android 3.1 and working reliably since Android 4.2. + +No root access, ADK, or special kernel drivers are required; all drivers are implemented in +Java. You get a raw serial port with `read()`, `write()`, and other basic +functions for use with your own protocols. + +## Quick Start + +**1.** [Link your project](https://github.com/mik3y/usb-serial-for-android/wiki/Building-From-Source) to the library. + +**2.** Copy [device_filter.xml](https://github.com/mik3y/usb-serial-for-android/blob/master/usbSerialExamples/src/main/res/xml/device_filter.xml) to your project's `res/xml/` directory. + +**3.** Configure your `AndroidManifest.xml` to notify your app when a device is attached (see [Android USB Host documentation](http://developer.android.com/guide/topics/connectivity/usb/host.html#discovering-d) for help). + +```xml + + + + + + +``` + +**4.** Use it! Example code snippet: + +```java +// Find all available drivers from attached devices. +UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); +List availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager); +if (availableDrivers.isEmpty()) { + return; +} + +// Open a connection to the first available driver. +UsbSerialDriver driver = availableDrivers.get(0); +UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); +if (connection == null) { + // You probably need to call UsbManager.requestPermission(driver.getDevice(), ..) + return; +} + +// Read some data! Most have just one port (port 0). +UsbSerialPort port = driver.getPorts().get(0); +try { + port.open(connection); + port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); + + byte buffer[] = new byte[16]; + int numBytesRead = port.read(buffer, 1000); + Log.d(TAG, "Read " + numBytesRead + " bytes."); +} catch (IOException e) { + // Deal with error. +} finally { + port.close(); +} +``` + +For a simple example, see the +[UsbSerialExamples project](https://github.com/mik3y/usb-serial-for-android/blob/master/usbSerialExamples) +in git, which is a simple application for reading and showing serial data. + +For a more complete example, see separate github project +[SimpleUsbTerminal](https://github.com/kai-morich/SimpleUsbTerminal) + +A [simple Arduino application](https://github.com/mik3y/usb-serial-for-android/blob/master/arduino) +is also available which can be used for testing. + +## Probing for Unrecognized Devices + +Sometimes you may need to do a little extra work to support devices which +usb-serial-for-android doesn't (yet) know about -- but which you know to be +compatible with one of the built-in drivers. This may be the case for a brand +new device or for one using a custom VID/PID pair. + +UsbSerialProber is a class to help you find and instantiate compatible +UsbSerialDrivers from the tree of connected UsbDevices. Normally, you will use +the default prober returned by ``UsbSerialProber.getDefaultProber()``, which +uses the built-in list of well-known VIDs and PIDs that are supported by our +drivers. + +To use your own set of rules, create and use a custom prober: + +```java +// Probe for our custom CDC devices, which use VID 0x1234 +// and PIDS 0x0001 and 0x0002. +ProbeTable customTable = new ProbeTable(); +customTable.addProduct(0x1234, 0x0001, CdcAcmSerialDriver.class); +customTable.addProduct(0x1234, 0x0002, CdcAcmSerialDriver.class); + +UsbSerialProber prober = new UsbSerialProber(customTable); +List drivers = prober.findAllDrivers(usbManager); +// ... +``` + +Of course, nothing requires you to use UsbSerialProber at all: you can +instantiate driver classes directly if you know what you're doing; just supply +a compatible UsbDevice. + +## Compatible Devices + +This library supports USB to serial converter chips: +* FTDI FT232, FT2232, ... +* Prolific PL2303 +* Silabs CP2102, CP2105, ... +* Qinheng CH340 + +and devices implementing the CDC/ACM protocol like +* Arduino using ATmega32U4 +* Digispark using V-USB software USB +* BBC micro:bit using ARM mbed DAPLink firmware +* ... + +## Author, License, and Copyright + +usb-serial-for-android is written and maintained by *mike wakerly* and *kai morich* + +This library is licensed under *LGPL Version 2.1*. Please see LICENSE.txt for the +complete license. + +Copyright 2011-2012, Google Inc. All Rights Reserved. + +Portions of this library are based on [libftdi](http://www.intra2net.com/en/developer/libftdi). +Please see FtdiSerialDriver.java for more information. + +## Help & Discussion + +For common problems, see the +[Troubleshooting](https://github.com/mik3y/usb-serial-for-android/wiki/Troubleshooting) +wiki page. + +Are you using the library? Let us know on the group and we'll add your project to +[ProjectsUsingUsbSerialForAndroid](https://github.com/mik3y/usb-serial-for-android/wiki/Projects-Using-usb-serial-for-android). From 2bee5b930b2f7f50e50895ec6e6e5d70b4881cc2 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sun, 6 Oct 2019 10:31:34 +0200 Subject: [PATCH 12/20] build with jitpack --- README.md | 18 ++- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 126 +++++++++++------- gradlew.bat | 30 +++-- usbSerialForAndroid/build.gradle | 82 +----------- .../publishToMavenLocal.gradle | 20 +++ .../src/main/AndroidManifest.xml | 4 +- .../com/hoho/android/usbserial/BuildInfo.java | 19 --- 10 files changed, 139 insertions(+), 166 deletions(-) create mode 100644 usbSerialForAndroid/publishToMavenLocal.gradle delete mode 100644 usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/BuildInfo.java diff --git a/README.md b/README.md index 9c20f4a..28ab52b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,23 @@ functions for use with your own protocols. ## Quick Start -**1.** [Link your project](https://github.com/mik3y/usb-serial-for-android/wiki/Building-From-Source) to the library. +**1.** Add library to your project: + +Add jitpack.io repository to your root build.gradle: +``` +allprojects { + repositories { + ... + maven { url 'https://jitpack.io' } + } +} +``` +Add library to dependencies +``` +dependencies { + implementation 'com.github.mik3y:usb-serial-for-android:Tag' +} +``` **2.** Copy [device_filter.xml](https://github.com/mik3y/usb-serial-for-android/blob/master/usbSerialExamples/src/main/res/xml/device_filter.xml) to your project's `res/xml/` directory. diff --git a/build.gradle b/build.gradle index f055c90..24bd6cf 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch literal 55616 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�J+L&6O)2CL%pxI@Gv&XXKx zY*yF%8{4kP(#nhT+`ZvX00#e(n`>Ot@(~Lo!Af#%Wa4rLayvtbPFQ^oOq_(I3XDMw zyO$Z~9@fSK>~*C{->PREgtn>sf(b1SbIo;~7XMD;0~Ew)t_gTumSpk7;n44nQUMFb z@QRcbnredr2F&!ax;`M4u$dPolnZaSYOTAE0#e&W?MOW0l-E)>Ap1m-uK8x>!uzce zNFa`R@{>A5pOez0UW0k(C!eB)y1#CK@w&!5ad=PDm}7=^TcjGeo3h6&ZGJzf)fAbhf3E`oy>TG6Yc4& zN%tFR<8G%ZA60W6W3ZA6`k9rStem{`?KV?SCHuG96?Dn9T!^X)6Du3Dm$>p7l({}! zOxe=qsBOCw6-ND1F-O$=5&-&H3J;vCwD({!Lt?BY>~UH?K#&xnfpgQ!YvZ;3lFxmVX`Yf#Rqoxk=U3Kh@OJF+gKtN-Y6XG*0 z#yi!q1k>F2uFDDr!Iym14S^1e6gPGXJB)(y@kN2_!l7(z$c}cnLxT;YP+C8PdtX$7 z4DJh(+$Yq*qPoO?fLPvTi&~mDA0S83qZtfVr6M&0c*zPk2rV+ezT+%Hx1cE$6^X;p zgkUHXCQ(H%X(dUhCs3}?|10RXLJ{uN-c)H}&|-^>)>$stWC}GC9W0`zJHydpj!Z>` zmN!~animXQLGAE_Wj}7Eh;3y}yORjG!r}3zx2r83zElo{q?06N80EPZVSQe*(=_4l zZAOL!6qYx@v>UV9OTrlh)p7;X-ec+5-No_}knAe+`o=$SbIZhI91x4Wk&8YM5vCPu zNpZg%L5@VU5{@&6arOH1_u41P5_9XxT7trwwK`GZhvqMG+%Ke&w6iW>fh(|(l-qKH z?tGk9&my>n^k4wn)B18mZj?j|>_Pt|7X()W^ps36EXRmT)!;29A$ZP{@Y|GS@iaOT zWd{E7nB$iI#%!VmvT!y>P1Y^Ed2mqn^E5wHuRp_09G=2QYhc#@*5HbD`>=s!gBr5Z zV+s7zdEM2#u5Pw||L3-;JH0O1`>t*vjE$qr$qFaEw9#tLkLn_8=(KPQM$ajZMF&(D zu(tFf&l`Dz>bc6TnYjw1=eC-ruCS+fYuyyY5@0X{34J3<{+`Jb zELT+R1wb_K(VR@!i8@Q@%aMN6b%Uamrjs{cy|D>kXINne04Rjb50vOJfmI z=A~pE%5R!vS`Q&Si4SOD{E(BikeV|HJR9+g$!;BQb}h;oKG%_`WK8yUDqNGaQ?oy< z@Dc>_#qQ*ebFW*dh(k-|zB1vpjDF!x?hNHJyv#7XEK)xuTEURZ6$o<1+fN^=TskB7 zSfgB({@-fW#w@aD!FM}<%Xcroi!ZoC_ZVtA3v1C!I_9?*Ja>g z$P`3Sc>{!*i4_jVsI`) zCH)})1!jb?rc+Ia6n}ICUUpf}@|y3s{|Y?Kefc^Q3`l(VVRt!cjd+OpYL2{!@eu)r zA_}2QkWEb@NH7DjMd3|YJe9oPsK!cL#aM|CBcOFhOyP?iiyY0A5(Zz2fg@d5!SP9! zWt)y@<0JbW@PyXGh`?cv4`@M*dq=eRi1!67|DbVWPrbvK!$_*Um_zD|Jf!=UX#j;p z%bWyI51f+J)1c7rcLT2-r_Fk?#sNTKiUVE4lp=_1drJIvo(IxlYo=z^ zsQiF^FF0ZfvO|leU|;+0?w*a+YnQN~Q}b!6ZtP%`cdICnzRq4#)fRDH(lpkT)c7{EZoEO{PzCS zA?YeN)qz-l;fe2X?x{MWTjYa#M-}1&8+LZ}T3P`J{l%(NYwX&k!1qZJ{Mkiw-{{u8&fgFP+b zs4-5-g*o`k^#lDZvlDDqRHKlnjckBtuk@h&Rc`$3Rb+f`RBSw5CsO{>`y z8OGWFH=Y$*U7sUf(2RH4K{nWN-cVW@l=u>TpQS@+Qqy?)nLGecPuu`{D8@y=LNOz; zIZJ4MxOL!vsldU_j!{|Fvp-UC2T2opl{}+RJ9bn)L-TW4Ml)~;-;Jjs;z- zw|}T?$m-2ws{%~Al-7gZLY;c?IKzR-SMZ}(-cG)B&Xf6cBz}8Vo zOBUUDc#(1fg=zGI{ieRcc9^{G3;T=D z2Yb{F-G9N#)!mmxZ}?I<*xJ~N#t>WI&Cn49OYQQ4q_4ci4pD)zgVdox&J*TJwagW^xr8SnTxf#erPAdl zjPZpkH2_@fMK}}@K5!k%i_m&I+#{EfFYBHm@u$3F(VNlnhOZa0TEaEdItFkv`fGK_TR5W8TzdS;3UlikM5=hAR#JMUl0)LGtv*Iq8iV9wO@g`ubl1&v`^JskZMqSh&E2?UHHM)pf43i6BWtp#Rh{3-no3J zW!Fdb1*q3M(q&;>F8sH~j4_Opncd9L@*OHC$|~{)1pew=HSPxn##Tl>03*aOq@pwR zj`&~f-aoOhe7p1gUzCfLS-1O={a>K;jtxYV(euJo&9;5LVMxNOK~A@o2rAYIN2E$W zF@CD|#U5_SfxNmT^5UfS*Z-f^Vr!yr637n_5RGI)d1=7Tifgq~Yul!7i&uor)#jnD ztS|~VaZ4x$p)ZwO60t(p212rY^q0(_60lHkfKZ)}HEgj0$#*@|E~0V2)^jbz9_fM@<)^Ze5&?CAlB zzaG<(7Tb&z41XxmUDK^i`U`H zD5*=T^n|PM5Hwh?$Exsp`4VbFXehKj6f&Y^?oG3<_7)i%}6fh2TB*{|MuLH zOk{Aep+P`o5kWwR|GS|L3i|(TXbaVh1fW@9`m3 z%ig(0)|h5|i+lOId*qXU=5scc3djdQ47h)&nwf+)fN?KuGvWYr&5o!X)sLOz#)@De z#?^BMz-Rb;k* z@F?az`2rtK(akuTulA@Wup_3P<~1v7=G7ZycTTK3$f3d$bWiP`K1Z!VRj!XdCI=-+ z6*f-|h!3%+4g~N+>}dkQAQUHG3!|1^6JyHF_(+e&QHX@2Gs}$P&E3YC@nG=pQR8gr zuG?#KjJmOh56)jB4|;eVGVCcTyVK|koxYZj@m2pG-jf!4C@5~z3r9(7@mpucRcGIR zgBfuVNMvBDCCbLfp5^f)$A8`wTk1B47_Q2cBj{{!*#kB%0$Xa!o5lu;YR}G{47YXh zlIBOeG`OaW_|e zT-Ic|%*kFQYteQ%V=d%NNcx#<)}fv@YA_R0mz!C;a*-dC=I>@LGREcq;N7)i%+!>Y zW4$0qPXThKN*>!9Sq67uaEgYSlW3@=)=}soEpToJp44j4tjBP$rc4nS<+K$`ZHEi* z#x`rkh|Rd{$LKcfYgSSVZh7jcx|~J?S#;N#E%ok zTm&#uWAiY{iOlub(E}(biGnj8mE-t_S&(0M{;akC_vNA4;j1u)jWBlvfz4>m6&(~t zp0mUh3xmyQ#T5^CeCDhm$tM)6#?CQPk|nYtJ2WW1&_j2Gj*t#?L7)bnS8++^hX=*{ zvV0}?FFjcER39@#=pS@2`%oB0|1cOP6az334iIMkjoFXNo$Hj48Ovwu30ottp&sVf zrl<>KG?tMGEksus~oRHiEhu!nxy4?M>)_qV$_kIUlbOOukRLviI9xKn&aQ|r&9RxgIIVVzSKI5BztkdK73RG7D!LK#9$ zH2h%$DK4@0^EWb}gPjC(9^)Rf!JcWpsrIdir6R<=GEqp3+R)F~V&a92Vvbm4AdGVU zosT2Tfy!_R!d4-qoeog=sgk{kYqVoD(4%y~lcP`(ZF*qu_(8blj>!k>e7b4ZNVdpYcrJq|JcNS$(|>w5 zCLtuk9RbJ*u&@Sr7@@lxle#CO-ciVOcd|?@Aay$8ChiS~>;mGzy$X|2UlI|1cD@pA zyh5wKQ5kVboAZg=^NF(MlY=lS9r2mftGI_Ytx@>|H?_lrfzV6FR|B!k9TPrclIC6( z4fvQZ*-5hxd zoB;r-^FWWmznFu4+p@q}er-HY#X}4z3CgGse;W?{+DE_aX;|qQeQ;8xbeO4A@Ie+JV*cNX`4lyK9s}j?A$qq zt%P0OCmYp&rwy$=V5B*Hw=VN^hSdMUR7}p@1{5v>Lrr*wE?qmVJoKAa38aiRnr>gb z)7F$t-S&9TM4SQ2=Ib5Pe_Frt@6IX2QZ=ge$T@ZhNa4pj$1>gQGxeujrrXhqw5C|s zyPvV!&A17}q@v037Mt24g6gCfZ5USg=_X=d=$ZF}jBE@BrU&<+n9+LAQ&O>Os6(%~ z1KX&qOqmS)8ohpX^>NNOjyqOg@Z^z)2i090T5%NphH?cE($pR`L|J2XiO5yJNh}e< zX7{_R6EN(k$< zlcGYl2tpz9$1#tvsJqMBT)_LH@!&L5j~-Xcn^zK1Gpne$$3F@+8obu!CmZc|S~4M# z^F3r@*_lEByL2~>wgiVp2gqMx1vC%KSSUjr+7^oOA6nv3*E_OOS#-s-waR%#eP!F|0fITKZ}Rumsxz-_Z~X` z{!t|__>m=>czus6fO9|Tim(x#Q$hn5A4DB-ub{UAmHy+%z!wmXqN>;BDDWsqXuf;( z=?OZ46b>ya7IJQD-?nd8o6#^Kza^u8YcoS>g}>!SdYANlN)y4>yt*9NSxF%_4=HU- zt*D_xj}}=uJo8FL7K-rDX`V@Sjl(CtNEDIQk31?pC?aJm`I3^p=-=ZO|}C6R{?feMfZ>V~?@tehl}M$Uv%y^nM6&Kb$46oZbaj zb}WtJhFkF)D7n`1Uo^FX5Oj3rWAzc`h-5bMm1-K4j5G^z>KQR++);MO(M>l5Suy5u zrlQN-{s7{elMghKYo#7*YvRQbYI9etWxt2gDoTP4W{O|8Q-^7gnYh(Aeh;er?O zy-c~{h^qu1+Vl~sbguKnB^7DN)sGz7-4<=v002qlrjzs!bjB<_wP@TrdHbnO`Y@*U zYLVM~xTB#0do7>mIRCkXXcq4gbDd|=VIq|Wj(Xuy7z8-1rG^Mx z9Kh^BPZ)WOq;SpKzvl>}s&ow2>E~UcFu50ZCOHBVF8^YaFqaO|Do&ujYPykm6F+0j zWvg+08C*K?&J4EId%!5Mrj?dLFc}0fH!%-3$&G*&u{XuTHAtcF0p-x&lCTLpQv!-P z4=lsG!X`v6X^Y}MZiPf}#Uw3>HLDnic|a*h)iIo#SRcD5?6C^K_K*r8E{K)u%E?j| zbXGJ<5t}IUE6@=iEVPm*<<71{k$~X_m&0)qgpxe91ZfhJi6FoM5@w7%!#So+RFLxo z%R%TU7H_EotpJ0MUr0G_?q9oN>g_e0@pX0hlCLOXI;gx6Nx(MoRKs2aSi@(Vsgwby z`ucXml)+J{D@3w^xb_^YBs&m`e`mGBdI(d?+?-4bmx8GNmT^PB4^2!ZaeI!bk3w-IT#=yHDPx zz19y*W<&YhgGN$}H3L%aXWVN=Y^=Flce7vmyzaIfcAv)Q823QX?xzQkP7$!I7a9vp z76QV^TQhjl)U#}r$D`uZ!vg6qCJeKQ)A9uk=YQMp7-LmRje7%D`08|aag|%CS#+Fi zh6IrE)c#UcK!h(^NZsXvFS#}+=@*KQ6cT;8Hz3I1=mpXkpI*20y%ML72_Q<^?v+^NK+YgkBWSAX9|le$8-zd*3V zp3R1lg;gV){xDX}2yLDclDBt-u9vBd^=&;_ufkQ@v+u0fOXJHB1)72?HrAO0A9($W zGds}jj&tDdgFmiZ;U`$~L#jF|2B%0cm(o9*X>c00>QVvJ&|?gHGPYt7Jom$7VQo3% z3>v+LS>l+#=MJ*)iv&v?V(@Dt=)uT#8*OHqTInGD>Hz3q~`cDwUXT3Fe4< zq~s$0AQmo2Ea7BW?)V))|Izh~tYhnQf49?>#vjaFm`ggVyWw(@M_kw1C*m{3fXs;G zU9e*1oBt0WIv~kBJ)UrKlyQPO@hFYcCKs$!Le<%55U|PR?`@?EySgxH!r4T-ol#;E%#LljKAm7B7 zl>8@+bqCTFcC6Ac?tz)_=Y4YqoE?FQDJvSG$d6*eV}7FOWuhI!ABqWY5dXhT^q&QL zPKxl5{O+IzSW69x2x97JDE;e-Q&pAEdo~oVWdV2le6K z{X677{0s{0I*ZYk_>Oq+0QXr?Ie*Z5AwZF^5>px3`%}#c**!iCJXDt399%sg&OLjS zv<%V}&8qZtDnLTCs$=rZm^c?0vOkhe^CTiZj{Fz|i10~giL1oIod|_(&WL=vwRjy@<)nt zh>1vO!DGB8^0og=|CHEoleP{qIImAQ^An+IaD4sTKdyO8-AlJ}!Uor$QE}we9BwB+ zHU3mG%U3VHXO(hMndq~^SF>2`oMU_ScHhk2qjlyt@b9!Z7(q2RK}_ckPgh1E*f=e@ zuL=AQNoHR1zkHD;LwUY2+aX$;rBvxdDDXDx-U|V=&L5;nN7c=^ZG8BGR*L&`|VewMg;n z+3TIp?|m=7mfynMy*YcQAyV+~S@vV@-FBbL-`U&IuROpHA>Q58MhK6(0&?PFR1*|{ z95%65jP%2Umw~MTo`GJOqBItwiO3K#_S$|0JcI}HQz%oPRv_cSl$>k3++qLFmJgY) z`k1#be~gcr?XVxqH^by%vfcDMW@a>W7k1A-w*4#c?g*;qLun}f^_7cIkmv}DAO&cG z?ba~#3Im|@@}i2#e|Ry%j^DSC`}+uh{Q!NDeI>84(1Bh6TmrPEIAn(2;F9e${gSj6)x+pBcm9Bz8DC9i-`N1-> zi=yR`Y$c!3r~d7A8f*s))f@wKH(0uFzT-9aU7+2UkHaes+iEa2*j|agPN3E6FIhLw zSCV=v^z$11w)oW&t{(BH)4B#adydr}*oSfyuFo===}n;I60rlDH9gmEZn`e?vo)|& z_$xPl!q_y@j4c;-22Cg80ux9u@o+->dtZnb+Bsc2vIj||c0DbRi-UkE3d*UE;zArPOly;b+{~Hp9?ex5jqkJDZy8#!<)9+T8p{WS^-3cQvgkRgi$7f)8G(REfA}-4%cColl{B^rV z8`ieSEA%J|v|Pb^G#wC}w4NBEu(QTeLn#AoMGSj3TtCfH^SND53IJ;8Bh52QWUYJe zL}4@;gWNF9u(Ku#gD##DLyxcq1JSVle+B#T z*TI4ZB5`-056VE6?$0?-@j>w(ng)O3(Ylnxh%1gi>tMxQRr~B8q3LT793?B*aX4$q zZ3LwGBP-@4-eC1oo~nbobRTvf>O*_r%f_iy`zX7}9nWCT)`4;4ZE<*ID-ez|Q5nsPi2^leu03l!(J9kw&hJL`f^*WrHUm7MxX@#wXT-ilP$WZcNJ`}107#sHtB6e zd3Cx$`ZjjhybfPq0B>!WK`u1E#7<-gd!kqTd1D&91t;d48R~F<`M@AK7wOl%K@}Kn zi6PS*1*hxG=(>}o1D2&|{`LFt15J`UWZ)kXPJ8z>--@n6eHEJQT_fLdRi6XwNs z4;v&)oG}MhOiSW>*hite2}Q+)Kq~rn19A+~ljRAMMJz_5FP+(F#VK%G6f%3)!p=|B zU&U$S@c!+fEXuZ1z*5-sGr>IDK>w7*!Z0hG205n{!ixhTGI}v~KqPD`0k4 zH#3+BLzl35jJ2`8H%D@@k`p@BKgoSjao1VQvxJChNHz4Oba3eJqV0j7)osDsop+FI zLw8eBIC^|@hy^Ck8`%wk7`0JAndK3hOp!5U=>mW|B+NpIBlN*?39ZEP5p8JUavn`X ztQv+k|DJ4D>?$?Ea)j@PHeVFYJr+k)a)(TgNUbMc>>_DL$T?~(qv=xsytfgf=anzG5%>A0RD48NtU9<$JXLlTVH2=`_^%+(@Dvi42@r+`k zRea+fAZ2*?N((GsW%8ycs4aRtsiOsR4*CJ)b|szxc+v_OX4eza%Vyf=KDAv1YZ}0R zcpUFKoPXYI35yooa0s6R=0X)p9Q@6AKsC|h2IzKBeX`B(ku`M12Udd2>uug~v&)rT zEwMunGUM-3!)!%#b~v$#6olp?L1n@{^#A(YFzmEpIZDu5ULFDR z5p_q)f4|6-@(_pI&>$n=v`7ugYI>y_0qE_~YfQuj<4*UG6r>A*Zs zs4wI@6}o@7+&$6TfE;J{jHV*A7eiG0QQ@Rd0*C`vgRrk)n$hvZ$KV0i1*})?ZAfL9 zDoJ|iM)@YPpT;eXm7P3l`W#?R=E61^g!e7C_ZKYMmHS95?Z5&pB_r5F;ZW-NU|3bh za7`olSi|KjsIc+_CV@+GZSJ-Mth&&c_@x(A;(xf;SZ01O_T`K;!&-`a^?_ejykX-a z5C0P!`JeATo()Fh%(vp-N@h$I1gP67E-9h;+pjGs^I$os0wKiUaUIM8h~SZPj4?2o zGKud^uxIElbQax5@(&OXqJIrY8NXh~ljLo2xHQNdi2BU&pK;H1zVx&J>ij<;l?LKs z*iKEK9yAfZQ&1B~jZ`KuQz!;9T8W8=f6)x2wT^&pFyb({CD@+)b&W}TJe#=Fji13P zZFD=-nORqtAzD@I3i`FYn}wCt>`O{8Tk|!d1gFWT9bK$m$;A^yD^Ixdt!jRZ9s)+~=c{X@HZ7Xh3dJ3$>3JQFo9ts2UvcUee(MEx*?T=a?!ERVnRTZ% zXKq%X39xO#JtY5Z8J6W-69}n`(uS>Zw~iv5wv|nbYq$TQin2=spgO>JJDCw-wjB+l zv#K*_)7x=InB!XDHHjZ3LW|kwANJH+<+4vHbiC{AaBnPdh8I;XEyjpjUO>>CmW&;3U4T8SQ%zu}@!j-)C`Phr`1^aGBtww`mGE>dxf)B=b;W33PcRoSK zeIxMS7$Z=k$tfEzsmc>I@el)Q4Xxym zxF1WUlS1HMu|h{|T40T4wOnVLeiVpjkPYE&picaKwVHmtq9d5+5%!W(rEW^uz-%E& zCW)01Y*Ky~uGAVm|MkC53&+MQln397-TqtX3jWveHhYx-(5pVBf+~#0&p%0K#3BTZ zbRQZ)HHid$P)HI%Qd+8xrW4v^xJIUWbZ#1!C71FG_)AX;J)Q>BKj3SBY;7nOm$R#( zhd&b*S%lo_;B@zP>m{ep?{?<+^RE34@n5VO#upj3AVMmV-}r0<(+9{Jn9sibMR19dIp=8sp~F7ZjY%c zr(1t0ak>8pPkf65KbdOr0Wqh&&Eag5XR`^3KMEJF6?RZOEuh#Kmrs-kvBc3U4$1GA z)5tCCGitGlJv60|7=XK}Ht2F(nmi!r>sXbgn^}N(RlTS9aZoxzcYF9@>5t+E0|D5yG9 zsg6FJDs+Y=0hVhb^_M4Fee5nKggKu+TH#?BEz$(TTI<7P_K;28E0Pbz*w);Mzvkor z9QR1PCv}p(O`c2ykYwZzw4?=0K(2$Xrgh1TvWo1wT8*wjbNs%YrSi!iXB-LK*AW{{%(<&OGtc3KK4~l8^@p+nd z+C00zWN-B;O`QF48JmTc$asnYn|isi4a)OsKYpb>!V2AK8J;d@^f0BDi1LlGnKWze z1r#~KCQ<_q`?7N;MK+EbfE3$_MS1?Wm#)X4lvLP`s`k80TbgF*WQ}$9vP{I4d0&%I z?z$8wbeE-W>SE0;oebAD^k!3ZXHhK%JbgOn({@E`OG#F!)6O!*&V)V60`kssZZe9> z1xO6DWb+DyC@w)pA`*5hY!(%^qJml)2Ahvg59&Z|kM(p#8^d!RfW_L&s`cX2|A(GZ zo$7cKHv1vn<@Hb=rb$hGVZ5w}qMu{i0N6ZTJO=)B-G;_8vB7fAq&ldb6k-TfOTF?M ziLJ3Reg1O?sSy=E$?Sm6r8A$aLV}IF zkU3{NaD;mKSH;Y_Qpd1OR+DL0AYG862YFuxg*kcCiKH zph(BS6b*v6QC31bFt``?IYLsxk_PtnFfZmWEP~ST)YO^&;w-Pd23G z<2?9?bk9{F1D5nJ!%JZ!!`If71F%+t3QTxxX`%7eWyxns1`U_2toZGfMOmYR25Sa0 zLT)Y=zMA5=HPp#TFktbBf4U^p7N;2@B^Q*yO|7QYbmO=`K6VH1y;CIxnHd&$*cA84 zBhBo+Jm%*(o`CKCu5*!xWq5bj_8%ih@k)3gw)U6g1(4`5n&pZ7<`do8R&5a!CVOOE z!`Ii!Z-N;}vOt*IIQqnxN{)&INaocBR zn>9nh8SODdbpbm`Fvsx+sW-N*w{DW9Z5bqwhON_HZUpSsuh}7SB1hH(`#v?*G#+1Gv~+Sq{8FV%|Z&Y2cgN#3DgpiS^K3-oB`6mo@0Il5ao;;Tz)#q7hrX4`1+mq4ZF^+l-oY0 zi!Mc8=+ZEhJsoWi)+om25L;BMbrlMA18gXagvl$7pz1Pj>ngjWy0{jFlkZ2qsf+BI zUH=&Ww~ooN)tf8-E@0- zzE=+Eu$u{U4RcEJuj2j^#ZZA)u=nC94~`$ma8~XFv$@@8c~krz|2{rH;rEe#`I^&-oWsaI}pD1SoY^?5%N^JTj zLyQidCnvOaoN}ReShnZuw_8<2I7#xzf~dx4w8Dieycki0Ig^A*Z(lkuHw22ie)A^s z4ExqlEuTuj&vgFePr*LjeX3M{p^)uI|6?Mq6Wpg1c2KnGd%Wj`h~3hh;}9c{`v~t(ZWGj(h;FZ&0xemS~P=6=VC@D>@RX2GrG|~8cjMR z#TTp5n3h&s{()eP_}lE<)@b%mA|)M?KZTgl^Z&7~bv^AfQoe=gIC3&l2LoUOfUAlA zPhx9gQLIsYJZ^_VeTgg0j5~~M-m^0Lh(aE3i`q%rQ3n>)hD0&JB4uGBQddT{a6YuO z6e26lk2N~5gbjv5Fpcx|eiqlE2dIhE$IFT%yV}9b$ogGyf8jmzwdh>@o}N1{2=>af zt5|><2G1IZ6oYZBoaRIwSOMs67%3(hRwx+VsibHOnUdBUZ1VgL7iweY%NC$ZfNtIE zq2zxT^5-b;LHm%#qgnbg_8S?==i|~%eYpeev7CIxdw#_oq2K$fTlP>W__IgWA2ezC z>x1};j$JwRBnlz+WsYQf%AWhm^7YXZI846Ah1{5LtjZTVn|iIbxdMtOax0@RlX$dR z|19pe=ny2%vs7autkmgbg(S!RlH4(qW{(PH7??=0LWhb@)1k5oekAt;I;~V$hQhcw zDYL_>b`|D2)5XOgbE~(tM47MOYLqU#)|jiF436c31piDZrO%Sp8HAFt(zKPuZ;x6L zKXfT>{b?an4}IQ?+XmnmjElVtvyYcPpI)WTX<(P-|FtObJ}VU=mjNN~IR`Yaiauk% zg1MI$@&28oDQlr7i)rJ~JbIEt5h9W-4@2x%AM(s|hiB4eb)T)`{^|;rny^E&6EplY zd%fXFA;oLVc%BNfL*aY*+dTI9nJA3Rm2I&>uTI&CO9=3Edkctci2*k=k+l#XJ8aXY zeSNn5rG%0qtG={47CTf@30D-HOz6s@ai3tOSpj`p?cQlEdv49+SW=Y^n6RX!C~{Xki81AQr)>^U!F)rfXkU?L)qQr2El8T zCbQWy+|Je8YYM<9Zy~QeezA~jXbREW50?iegl@yed+$UL`~C`hL$#u_|2+@uOsZ2? z{50KIYw45**;*DE6y_^VtJ&vZH!3&nw zMryQMPg_m8EP-(pub)F!5aIV6F#41i9Xi_kp+$xue*xJ7jzHQT2Sh5a5HOTm?JpjC z?aL-2twBkkGPWjqPq2rXujb(Qv?K_@0Yq}s+fz=~Hg*lP?;fADdsL1QiCeP|wZS={ z3k+1R6Iaef^Oh#$k-eHW zS|l^te4C;_)5QBsQQB0NvKFClY_RQPHMxKTPFq#w!+d{kt;MRQ`>A(( z?OnTG`SXFC>P{FCMLyK+$LYsva@v~E0!{BmnA@Zaw z{f_GG-dv?(mwHBiT*6OHo9w1&jl)`G85lc?wh%8WB$<}HECXrT7~=ZZX{xT}it7Gb_l&z+*P?)@5VS zrMlpU5Y|q|@|8E`&6L-)&SSw&Q3~zBnKe%6XOQt&}nJ+H@57z&uk9-<9jT)5{Ij(wqBcY=P`+%SR+?16PCJ*m*(0F%}a1Ej*HY zIz<-`n5tc0|gy)r~EY>8f_LGLoO2kLRpbKj0lsGIzbX~iRk zqoMO6*^8hT&L;GZ!OzD0z=lt}T~8*ADd6LP!WtN64A z&iPXi5>lC6_JG=7s4Wp$&AHJk$8NpY-zUUGfscXLIP5bcEz)_tKq3;M{?33KU`p>)kWZbBpX;Fn*zV%1qn3w88mq@j0Kw_n%j)V zkM778NbsF%=javXK4?QQ#h@0~0CEYU@wE039v+^GiJ(Sz!p!!vQQlz6nBV6cbsCGx zHq_rqtNzHlWDyNm=ixgfc`qUPAQ)5hBm@Kx|-)n3ijI()qD(k*chOAs!vDrw+?#`b?5d-Fc&BEQ+ z5wQ8NX$Cp?5l8!S7=gk(LwTsIDCI7BnQ{V=Ovt3+hnhcGw(U(U6%WajH~H^&iTsea zbF<;)@Tgmt5DYz%lMP z00ouYSRD`>(1+Xd(%kaWKAE9^A&o1W^(9j&dWKG@`Hagl<{ycnz1-VYDTyPneNH4B zZY=pbfGIZ7u;eq{2smOF6uNN!1)i(KZ^}gR2RxoGVctOliI`L0Y$nIzwb$J+xS$r- z90Lrjj{&rZ#0H8cpaYi+-~fp3YBO~Pj2FB>AS6{hSfC&U>fpx&Fmb=1?2>AUP^@qv zq3*Iu3I2m`Q&PcHzUECcT&a}g8Tx1zmuX1Jg{hR5of+Gf_^b%qoSj^)>MnVG@>=q= z);;LBPE8$oi$Zg~wRJ4yRna!PcyxYN=eGL$wh2>`Gg5hX66GFn_ZuK3Oggw%LGZ|& z{G>#Bw4JNUe_}XU9Im}Zl<91#&yRmRyU=ZrI?H)&nUUVlzjvV48KQJ2D83cXkA41C zaBMF?@Xa>IHiym@MJCEmJkr=1OTpNw`HCqjlCv$2og1jTw^$K!{QBKMA3Y{Y^Fi~7 zsJB<=oEC#qKHl|x9L@j)*2ry)Xa$#V#@7z1wL8Dy2O<#e)DQw0HUw0KYb2HH>|9B* zyPdXGbI$cLah&SduU|SyjYlSTbKA4B@U}TN|opa zHpOy|kFyc>BMAL~fb(@F#`G}yHG|A{JAtW2h3=*9czq);$2$CI?TNld{02@GvCZD1 z)FVR%dpr~l>>1dPY+?20)?aL`5WI?QqjgohI13$uENcn)MDU_}30vyLD^NI=Az+3} zXJw~{2<93&O#K%1&8COqA_WJA@j88@ovqn2tE{dTu}cU5^_a<%*G}0X`unh({8H^cQN(BZ#su8%fEx@NO_ZjNWN4ZH=b?>Cs zL>_08b1Z9^a%eTOW7w8hMW!*6mwe;HDwRLBHD$F~7aVb#iJQxvEFmXw7Xm8vU&!*r^T zg#^2$kfKLjaFnv_5|5+6jaWy}%wMlFQ6~K%38I(a6K;mnaN`KSn%O2{P*TR&9br6jkn`Hbq+N)69hq^RuB6`*+j(0?|iA0ks+ZUJc|s+3fO9pl`f0C3hQey(e2E! zG1kIN$S(I~YkMPTfCD7hbzq;$hOh_Ye{(SPLZ%xPuzPvB zXl!4N;n2*f20KzQUBHQE>M|i$EC&l(bnG-0l5uZ%$SI`8@!dp%NtLM9Hns1_`)^Em z!rfxM!Jp9gPsGJPMLA2Qt8~N1#Nzkly$9goIpeO_j*B%<<*2Ad&^?WCM1BM36?VgKo-B07NDX>q6=w{ivv~JfvnG<@z zR=AlP6DW2Z-$(Gl^e4$GBa#SZc1h~DP{C`WBOPs@=)MP}_N0m__m1z`J4eV*L~8)B zjNusOz)#zdVSy0{fg(S6*wm5eIol3#w0XAhzJ8r|zuO$$fm!Qs^S4E6|HkBRbp@I!S&SGe{Ix@?eNO2Q{;%rVBA(;SfWYQm^IV)Ep$dG=;{*hYt@Y+g zXZutU3drV8QS9IAoaOnK&VYh1&z!*}`<%0PeX*%0-E=`A?eZu-xLF1H*eJ6QS8A*d zrm(eUHY_I$A9&rd8kg<*2ufERu5tpKCx|sfq>~f*vDfa1&2BWB+GMr{mrPW`CA3?_ zR%uB3=e%_mJi<8` zU8|PU+6tR5A@qqnvI}*90U{)FD=P{_>ZA*1y((8oUn`cdRR=IQFG5P~bzAj=>5<}a z;dvyxXS|OS(GV@dGY0AS7PzHDJaOlw#glM zKJJewOIqMmQ~i+V+6%5+e7BFe4ttRIQKC3BFd&Mkfz(jij3UQ&{GwFLi8ICfJEh)zwmSGQkk7;Ry-u|s!*$+5*)tccG@~3?_BIR|3h?dWI2FX_?mIB6H&|)4hj6| zfQ242gWZd4Ua%~;9Iuw(#6*!OzdLHB=b z40|!=EcvH3Thm0hg|>)CT1-Mia;?EHp)$PX$cmUG3;O7Mrvu?jr1Xr_BG@pj5?IVq zZlKJB64OzEUgcD}C;b^-(>Pb!&`KNUCf+&Nmbfg9P6*%(qO#-*ymJ-?YoA#H8d+P* zK6%)@4C+)eo4Ix7m#>b)L$Ol>Bm`G#5oMYO4Ghuhb`k1tgv4WwpWyDRPb$`*yD#yY zf{KsGE`LB0x{2)YT&aqwF4Sxvei1J;;Qx{=J$Iw3qN?rZW%*XZgEYE#rvJq4W`eg+ z!u!3aZ{a7v_$`5Q@Yj@bAu2?+$LZSx$3nO4_(h(do?K%68{|-|n=Fmob0OL#+I&#>}rU z(JTc2zVgbemA-jUrudi@+rOD;I+VU58{(&0Z=@CkAdjHJX6fwY%Mg*|2;RnyqO98D zL<{^XS6bdQM&bj*eKA6m8P3>Y?t-{rOXygLQXCRgAwAd`7=caA+UGCwWHfEQf&fOtzwovF_XN^ZeUHe@VWL}J$ZEl5R_=gv!N0oiMXgtw5*@c?C>3(^CPC&raN(y0@^ zgeQy#A=yehlWxre>)Ta1;DLiDM4L>U4SS&&M|W268}3XPOV$0OOR57Ri7}C2?51(6 zlC&!^JG(qh`HHEjoT;3^w%O3kNAOusj1KENK2I1S)Xx_Jl|5{7S43{?s1sgL_ZzdF zfDS1;VwohRoewIcLa^yKv~1EULfP?U+&yi6Pw<{>{`^jC6!;aNo5%}oz~F4wn+fHK zO(&Pom?vJ^f$`$;Jmg zOGB25TAvElG9syq%u?!4I@|q&aGhtu4f8h_yn~^alnfOBWRBeV3*#QgVp&aeY9pJr zvYRW%J=Ng*ys>Ec?MMMHRh1KF+qcT8&;bDQ>JK@OArcP>{Byp^#b>B*L6w=gE(&BOm8*g?)1hvh5s;M4?Dm19+H% z6{-X3+NlV^0Ogr}OWQiw4RVDy#I5-pbtaiwxv{e)JMy=w{$0b%Ebml-5Ej$T(L%N8 zggR)lr{i zGjNEQmW0x2r{ZbyNh_#jN@SN$(BRijK@$&f$!p|6!s6rJZ)vrQ8ok`tY-{fK|K@)TQVvkSSVhS*=A}23ngdIrHgO_IvK>snFZGLcAxtwx6SB~X5E>3}po57YQ8+!k5I zwf?n_>MPLFe-#wH3GPnMH`P~CL>lAvPWv97(|*FH9d>b5+V1!m`o376LoSWa!d3{7 zGe4OzLaFS<~91l{_ zi)=dRXT;A-pDg%%a$2jP7NOqRJu*HLUhm3?fZy{n0HR&;JAmxF&~ z9^_Q)0|IA5?oq+2Weyqu@Nu@GrY3W=Lr0LbNsZrDq)x+8C(1?RyxwIx7q|kNWa}J@onNEK?Pjv$)K{ zV%_F%-Q-88F|>R(I2FYJUqwVBVwvHDSQDABxaS~ylt@zs_}dBt@`m?6(NR1pd9k`N zM>!&L2#xWw%}f2*`YI>>V3CL?GsBi1h=gYGLkv(6;EH?fT@c{PgA#W=1xsb9D3B87?2V49R_(W67Lmp^HJbp?1=~U!j){-Ak zg(ts&Hc07#qdvj_kg93_a|7YgJ`5pU*bl@2QN+aDyf8KuZ@588Oy}@cHaBbHnQ|^S zsdLI(eQ)w``Mhw_PE=FDhLSJU@d0d#oBtuc!%YTZpRZQPt|(%tEb_cVD6tE&`A4%jm_jO_me z(SMWQyz5b~<>FCd3bZ1LB8tSPA*b0qqCYYQc2q17g5!!U4Y0E7d-Se3PVE?UFT(|G zacP)w3U zpDT~)6Yg^cgeix?EK%iDEk+J(P$TTT#jKLmpaU;SBqKCt$NLY*Q-UEDq?b-SRHA_wILA;0Z(lUx<(^4%StnR|OG&R4SviKln`kp8eVZk>wslm&5hgi5 zjgO7Wo`@EzT!F-I4>L_UuxD??vP(~-VZ}Fd7Ua|8lcI&*cUEFRLW-ZWB>0iPgF4;` zO`yP;mr?-8@#nMX_?Oz8%!Ju-0;vCHKm*5oXn}^ANI?5ESb*w@(jNuVDb)JJI{l$! zy0ifGMP^g_P1>+N0!&J9OeC0R|)7Q%g)7hBrMo&$J9-bdjmJO&ktS8Q3k4}gWt z;ZAx8XManRI-UyPRJX*zN5K8ghIHzZb)Z zLRgFu9={`A{BC><6Oyq(v4uM*cnKpC=1M{K*^-jPEq|7wdOz}8w#tZ@qm;>?iMfvS zhe&-O#X^99R~`xbNQMZ9=dcu?-IsLY0~GPo(C-2Q%cgioPE7?jmqQ*c%*!wnCvG{k z*p`LGyFa!<1Roob9%u0s6Z_;0l$26u^M|6x(+LYK((`!Bv@h@+YP`dO_}t)Jb8c>!6HRATe4;Ec6v$ z5|)+^2~0GvB_e;s20n9#jM;b#Y;clUR~hw6oJ~i&v)*ClW#N=LvRIOcox#LrL(mEs zN<@sxtRpALbmgY*G=1zrX+INXxa)ek-u%go*^O!7JKtbE7o!x5sbo`Pof zBk>c8OSV6mNo9d{*QVa}g8A7ez(a=5?N-GH0IWTPX1_GbYUYCTM)P>_RnFRehU2iYe1+OurW9EBs& zQ+}cKHAWy0(Sk8rQgq%BYr=%HOPj`E$*vFe2O2rDk64Mnl8mjqS1}N-v&`7^e&CtW z8?b*i){e#<(h>aJV53SupI)etb3i zM^q?fs?UyScrtby){n))!em*Ot^!Y)I_$Di9Q;DhQf+8eYi%MXy!&#EZ>dmxSii4I5ne15Ee+6r zFTX|}SZIo0(?oQo*u+s8I!yL(DiHW`va<=$?=sM2@%Uy>1h}q|9%cIK#+B7&0l@X~ zUrGQ4PQa5^Ui^IirqFc6P^af3{+R@Ne6otDN*~h)l`6)(9?fe>3-;@CNl9&K=w>(x zY3#Kj*6$Ua%=p|^NO2B+XC~7Q1L# zaI+L&{kmL7LV)l7m{NW=$n0&ae@1gvwe+KwPn^Y-<0YGFs+;6!r=?){qnny`jk;Z|Id$LYKDU)46VGt1C%qq}ewv*DDR*_F z?i;~@ymqHMaQ;Edhj&Vi zYqtN~%l9)DuJ4mj-Pt&klF*9c?QVA9qc|igP4SgL7g=HFS=krAi$*Xw2o_`|k*Py- z4`u!{qjJ+)`9*_67HOZ3?=@SQpE}c5(7$3|K zE5!pjkXSaF=-A!4zJ&W|U?b8?HQFln0n#6yd>@m9=ck7C!5MCq z`S$mtoOaQ~fO$A{OvIk9&>o41pC@S5GC`3tm6Blq9JGQ@QRBh(*vS@e43Tc=pp!Fi z9Zzl@FF##ChF%eAx5%ICTkrWR;K(^<_Cd(mty6|}q<^9R^*IN9&+s#oWBQ~w=QYL_ zGF0}}h%HdN_(WU0dPlp9S$yT7f9lsIo3O+Ew8-l_5HPb9yA?Efl7Zfi14%pC0lLbd zS@WsBf%$}@Ko3id4mX#1C<3?jT?rH$xn)m2OyDO80qu}KvnDe-0O1Es`)y9Re4L~Y z=+*d-<2MCZa^9(#d=Hst-fMgu9nap+;D+do^d|4#sk|i*5J$ad&Cc5iiAa}Y%8&EQ z>SJc2anSWW``XCZakE{Wzu6uEfJMOY8nJQE6f3MAzD`;F6;!Mlbi~ZKUyRUgY%sim zVAkxL98fdJBvIiRQyI0xUO*+^g<+sA#`$4h4NbT1C~;X69S1OBW#hDmy~ItQP&UtH zX2&iw;Di_DArO$b%~=0&KcxGRX3n~B`~!Rc&Xs!0grSE1ds|K+BVDyO089H%ss!&? zQ&=*RB}tUfBr}BkozpUea9cZd(q@hrV{g|>xPI>Q2r>QWl|1(~G0g zKA=Awf2kN}SR<#Sl;)$X1;r#SH=f=j?XHU}!FOcF?jZN|Tm48?UwufPAQJfjO+XwK z--k3rm3CtveG>h5z;&^{LkeerRk7Na?A!igaS}}8ndHTpetMGgk|XDY=gowxM5Ybc zB7uZ0BggSLqFx#{Y#kkUX>?0XAk~G;e)PJF8{=*q4IgFuJ5s`5gDae7-uTYu5`FLK z7zr=e69OQ=R-u~w{+Qf%hZ4O||DBKY^Yg#v*ZR&%rxZ}{{swZU{a@D^FhG%M96*^m zXnClDCe#2@Jaw(Y&h=e~HAZj7Lf{%GLcn07LQb3J{0?Y1(ZOwK**%?&l{tt1y@jeM z8BHbZhiaOko1`~sPewgPfk ziQZZmY|vWsHC9_;J-Qa#bcQc<0RI|_I zyCMy($9kmBxaInERGy=bOwVt+yNm7J8@Blp9sXq9JP26RQ2Z4H z2s6SS>C&gf<9aTk&PayX`*R8v07)9`1Zg9V$LubEla zf<#486NK-J+{~f69ee$qb>0X`<-N0r4Y8=+8(>7Ikmt62dlra)%t!%{_9dNA7GwUJ z^ot2KdX%i!j7_`>Y3FOmu~g0@6&FR12mBZC3xqoiY>vVX(Yx`DNU?JDsz}<|jqnZj z@^@Jq41Ew;LP)hLQd=B(l3b!mJ`%^o?6B|L3TQc|J$?(lhm}2sDZj4zQY;5kAI7VD zILYkl9O-WdHMhjI)hIg4LbgNY^I?n;oqw39oc&Sf++5@U^r?YXv7c4f_ z91-Vpx0h%3gC^bFjCL-jj{Ghg+Cf~1tVz1R> zO|lKw-|O`Y_I2$@OyKMuLV|&L13Ues0Jf@_njllzYI}zo3t8mP_4BfUq6TncC8B7E z=y4d~wu#*H4T@S*GbVEjHs1ibBDv39W3_=v#n-y#_XW>iMDnJb+&;q5RDO3T{BSzl z1hSlM`jO>z{d9Hqi3Kd*;X|MHNM@dyfi#{I5k5W!jw#c+4wypY*m-yglT#_&JOCM= zU4w5HrgF?dwGT%4Gh3KqOhy!1J?}4~2K@Vzvnx;M5F;jYozH5$@7+ptzUj@VYS(C} zMz3VpSv07ZlxyRQvw-xSS=dUIJ-a{1@b0c~>zP@liw&i<5Om`k&09^&r@6>5s}Euf zrJB|?m+3U8@I)u%45JxLoTew$7Xox$TE^+PeAwv-LvpnjzUfq0L#1C(;h!9aDG|_- zinfGJdEkVIP0`JNGuoV!IzZ1kH!_tUS`(73ELT0X;QBK)1z>Xy@--3C7I zXVbEu#HUR2Q*j0vHYn`Iq-a19hcTy+z6bsuNhYx{P2UoLW{{^aPTlyn7tbBbJ8pji zy^(y_{h6l(whhe@qynkIe8QUh1;JZ z^3i+L8;_yrnaV4x!=!sztRE6E3?|s4W{vYoQ=^HRUQ3FwNW0L1=LUFyWBa%)e~H7V zdBx8S?>rZ+oh#B{KA4MRXS}Z|OklducV87SDAuS$aa&;Ae7t}$o!^>Ue?-0Q=uyn* zVWhasL6{PVf)31Uof<&R{(60R;~8tlV|iL+5Z((2X2DTD{{GA{>f@fhIAt8IFF~p+ z?x)`mYeHDa)%7vayBl8xP)AMJEz>xEdCvngF4I+64pe~QPl(RP+4oHi4^=e4$UK$y$-^A@FZ8Y}wNC0P(2c$7CT;#6F4J za>fMj(WtbWJsJheWT%~Em|b%;I_$e~GFjPhClR{Y6b?Deod+irqTXzM-1E|N@Yd@M zovw&;H9z3<&SFwI?5950GZ4)%=%qhIC-QTDV@^)ejo2vF$al3D5$1%w~U4|5VN)0dy{_abofiv2q`JbKogb zrKQB#!keLbwTN~%C+a#aG2e^aZAQ`v-@@L4mbJDECV^0vlUbxU8E4*m8B1#b-zNxr zuzqQRC2sclu{^6Sd{?GvW=^xCYj7P z;TZih#A_-Q=xFYdSBsn?M(A4_Gx2S9;}Exv$SMZf0hx0@CY=zr)dmFkYR9Sk-j%R~ z@LL?&5@u&(etX7Ltn~UGOwf2H)OH)4nv!Hkhl;2jpbo&~zSdPl^zu0VVv#E~uUjZ| zdb;KpbH8^=M$xL}`zxrdcXWj=$tKVXFVe!kkj35cy~^hHJxol8Y7{z zXOZC4AJ3i`)p$LWPU1t1%-nHW)##b6nSkmDoj;Ssz^L4&LQ-FTS?cyLtn5p!Y|gA4 zj3S}vtp@vKQ+#^iVx!(#zAqsUOr$lE>xQq0uYd>DAx7r_+*}_>kK}KnD+mImh+}TZ1%r zewXU-doL<>Q-b$Y4gpIcy^D1#yT;4)#@pPjriHKL$@>9?EJO-@G>mf+1A8JMOtxB# zCB$B#v(j%#kAp$HGsRB;qEE`*K{+Vw0fGch7Qi~4{%KmAY-l`lGFp$ye4xaGI$^XV zKEJ(++`5EbT@}aVOAekxHHjwIOlv>u%#qDgMKHAib5$hB8>V~0{kN|1F6awgLdJ-5 zHG@H5PxfSuqpuvdT00AZ(6W9=eUG&&8;$xGL$i8~h&ZIls6u*ab{1R&1np?vdpus0 zP{8J;87gzEoT3L6l45fbn`!skPif!E;bqxt7h8R>=@q*?7t(crYeVs^r8^Y(Q}62Z zTJ?X@79x|iIny5NqqQLs%eYl#T9T(XWH9rF>!y!fg>H6|Jumxj@0hm@WkCw zh++vu^4A$-3&Zyr9$)}6RHVePr-Nsazyo|Ufa?+^eli#|Y}AH2T}UJx&U~hUMk5pP z=8MgRpRD^6Ma`){2Prg;5T}1xbZIrLhwzr6mI6d`na&}(s2(J>aQ#T;?EYE2_$?{r zm=JTRI+rNAIKlYmwvYJEsh*U2WG8nXKd?r-;2m6)bg_J>l|__r3rE9SF49yW&mCZz zmm&vcjGjh9`5;cVqLg0S>Di`Owi6=y8)a^rh+S)jm_X84s!qCFeW8jb>$6a%r$Rpq#vMn!h{098@qW_`q2-@J&YzGd;K0l`Ihle@7DkR z?kW4{nV1$zd?ERO>qKJ>tV8P3$@dl_1fcBUnkM%NQRC*G-!Y%g0%C2ADbzgUaAuCr zguej1o(xaZ_a?%gCc1q(h(`E^*e{nj`fpV&e&-wZj-Y%j-wRz3hi|k8dy}IXyPnbu zUG|j^SA2fRsw0@q4^fhq4s!Hz^N?y#J#S8s!ZWOEPL$Rd4;*0Ci$>d)ys!ULMh6L# z1lKIQP;m3Z>wB~1st=en$epMpaIcT%|I^Pe`@8?~^NZ4tg8~kL1^L&wWdJGm1Ks*L zkpFy``<(J#1SDG>*wD`fh4bg9pZ$1Tk78+{FF!e&gA6tQx$@7?ps&F|Q^L!E_@6Tv z7#i54SUXT?fPnPh49NZ3e~v%Q!AsuQQ-O%Ku`ZbFDuSI53_P z8wh}f2eJ)fzQDb!!LwMPlo2F4C22qnyQDBv#IU7zdaTM0|u$!{v@?`tmOo> zAI5^J^>>AO!83;mU*tj+d#>n#d!5lbVU?p$@V8r}i z`IVh*pqc^|j1uHkn(mckKi(Jk=O`@j{TRWE zD*Y$FsKDROk2aRC#_6An|2VN01YNBP>MCsiy%uz?^-nbdI{XkJ`vbjl zR{RR0G67D6QUc8;FkirbyaI!P3BCf+OkcnsQ?xJOD~GGEKnx3@SQ-m(d4}l4$@Yt* zOY-qQ1MSK(Q%>7wB(SPoOcz^9rhW2KrA+y+E&A z;K(GXL<$umV&D3&EK8k)sEyVFsS0C;zgz@Ui#L{u8-eK z-*T&dX}eb2Tr;-uEN?KP3W!38emMqTiq)#rG<0!bYI-yv$2I zy?S1H1)Vm%NLJ242l`F@)y4l=I{&+lq*`BKB~OC?la0TZ0wC$HCxE36;Bg;0&N~HY V&}jHG&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -90,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +129,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +170,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec9973..15e1ee3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,14 +24,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +62,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +75,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/usbSerialForAndroid/build.gradle b/usbSerialForAndroid/build.gradle index bbb2b57..4961de7 100644 --- a/usbSerialForAndroid/build.gradle +++ b/usbSerialForAndroid/build.gradle @@ -1,6 +1,4 @@ apply plugin: 'com.android.library' -apply plugin: 'maven' -apply plugin: 'signing' android { compileSdkVersion 28 @@ -28,81 +26,7 @@ dependencies { androidTestImplementation 'org.apache.commons:commons-lang3:3.8.1' } -group = "com.hoho.android" -version = "0.2.0-SNAPSHOT" +// to build a .jar file use gradle task 'createFullJarRelease' -configurations { - archives { - extendsFrom configurations.default - } -} - -signing { - required { has("release") && gradle.taskGraph.hasTask("uploadArchives") } - sign configurations.archives -} - -def getRepositoryUsername() { - return hasProperty('sonatypeUsername') ? sonatypeUsername : "" -} - -def getRepositoryPassword() { - return hasProperty('sonatypePassword') ? sonatypePassword : "" -} - -def isReleaseBuild() { - return version.contains("SNAPSHOT") == false -} - -uploadArchives { - def sonatypeRepositoryUrl - if (isReleaseBuild()) { - println 'RELEASE BUILD' - sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL - : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" - } else { - println 'SNAPSHOT BUILD' - sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL - : "https://oss.sonatype.org/content/repositories/snapshots/" - } - - configuration = configurations.archives - repositories.mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: sonatypeRepositoryUrl) { - authentication(userName: getRepositoryUsername(), - password: getRepositoryPassword()) - } - - pom.artifactId = 'usb-serial-for-android' - pom.project { - name 'usb-serial-for-android' - packaging 'aar' - description 'USB Serial Driver Library for Android' - url 'https://github.com/mik3y/usb-serial-for-android' - - scm { - url 'scm:git@github.com:mik3y/usb-serial-for-android.git' - connection 'scm:git@github.com:mik3y/usb-serial-for-android.git' - developerConnection 'scm:git@github.com:mik3y/usb-serial-for-android.git' - } - - licenses { - license { - name 'GNU LGPL v2.1' - url 'http://www.gnu.org/licenses/lgpl-2.1.txt' - distribution 'repo' - } - } - - developers { - developer { - id 'mik3y' - name 'mik3y' - email 'opensource@hoho.com' - } - } - } - } -} +// to deploy into local maven repository use gradle task 'publishToMavenLocal' and enable: +//apply from: 'publishToMavenLocal.gradle' diff --git a/usbSerialForAndroid/publishToMavenLocal.gradle b/usbSerialForAndroid/publishToMavenLocal.gradle new file mode 100644 index 0000000..1212e0d --- /dev/null +++ b/usbSerialForAndroid/publishToMavenLocal.gradle @@ -0,0 +1,20 @@ +apply plugin: 'maven-publish' + +publishing { + publications { + maven(MavenPublication) { + groupId 'com.github.mik3y' + artifactId 'usb-serial-for-android' + version '1.x.0' + afterEvaluate { + artifact androidSourcesJar + artifact bundleReleaseAar + } + } + } +} + +task androidSourcesJar(type: Jar) { + classifier 'sources' + from android.sourceSets.main.java.srcDirs +} diff --git a/usbSerialForAndroid/src/main/AndroidManifest.xml b/usbSerialForAndroid/src/main/AndroidManifest.xml index fc20156..e579693 100644 --- a/usbSerialForAndroid/src/main/AndroidManifest.xml +++ b/usbSerialForAndroid/src/main/AndroidManifest.xml @@ -1,6 +1,4 @@ + package="com.hoho.android.usbserial"> diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/BuildInfo.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/BuildInfo.java deleted file mode 100644 index 1f0d363..0000000 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/BuildInfo.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.hoho.android.usbserial; - -/** - * Static container of information about this library. - */ -public final class BuildInfo { - - /** - * The current version of this library. Values are of the form - * "major.minor.micro[-suffix]". A suffix of "-pre" indicates a pre-release - * of the version preceeding it. - */ - public static final String VERSION = "0.2.0-pre"; - - private BuildInfo() { - throw new IllegalStateException("Non-instantiable class."); - } - -} From 9755a4cb87ce3ec1d2dcf3900ea6f92c414c428f Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sat, 12 Oct 2019 11:37:35 +0200 Subject: [PATCH 13/20] release preparations --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 28ab52b..1d37158 100644 --- a/README.md +++ b/README.md @@ -1,3 +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) + # usb-serial-for-android This is a driver library for communication with Arduinos and other USB serial hardware on @@ -14,7 +17,7 @@ functions for use with your own protocols. **1.** Add library to your project: Add jitpack.io repository to your root build.gradle: -``` +```gradle allprojects { repositories { ... @@ -23,7 +26,7 @@ allprojects { } ``` Add library to dependencies -``` +```gradle dependencies { implementation 'com.github.mik3y:usb-serial-for-android:Tag' } @@ -153,5 +156,5 @@ For common problems, see the [Troubleshooting](https://github.com/mik3y/usb-serial-for-android/wiki/Troubleshooting) wiki page. -Are you using the library? Let us know on the group and we'll add your project to +Are you using the library? Add your project to [ProjectsUsingUsbSerialForAndroid](https://github.com/mik3y/usb-serial-for-android/wiki/Projects-Using-usb-serial-for-android). From 54a3db115fc721d07545243c6104ee2ce6e8fa5e Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sun, 20 Oct 2019 22:12:56 +0200 Subject: [PATCH 14/20] show code coverage --- .idea/misc.xml | 14 +- README.md | 1 + build.gradle | 2 +- usbSerialExamples/build.gradle | 1 + usbSerialForAndroid/build.gradle | 10 +- usbSerialForAndroid/coverage.gradle | 45 ++++ .../hoho/android/usbserial/DeviceTest.java | 210 ++++++++---------- 7 files changed, 164 insertions(+), 119 deletions(-) create mode 100644 usbSerialForAndroid/coverage.gradle 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 From ac1fe407931655a9a955627737bdc4b5ad70d41c Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Mon, 21 Oct 2019 20:56:13 +0200 Subject: [PATCH 15/20] manage USB permission intent --- .../examples/DeviceListActivity.java | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/usbSerialExamples/src/main/java/com/hoho/android/usbserial/examples/DeviceListActivity.java b/usbSerialExamples/src/main/java/com/hoho/android/usbserial/examples/DeviceListActivity.java index c7804e0..7b6fc0a 100644 --- a/usbSerialExamples/src/main/java/com/hoho/android/usbserial/examples/DeviceListActivity.java +++ b/usbSerialExamples/src/main/java/com/hoho/android/usbserial/examples/DeviceListActivity.java @@ -22,7 +22,11 @@ package com.hoho.android.usbserial.examples; import android.app.Activity; +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.UsbManager; import android.os.AsyncTask; @@ -39,6 +43,7 @@ import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; import android.widget.TwoLineListItem; import com.hoho.android.usbserial.driver.UsbSerialDriver; @@ -59,12 +64,14 @@ public class DeviceListActivity extends Activity { private final String TAG = DeviceListActivity.class.getSimpleName(); private UsbManager mUsbManager; + private UsbSerialPort mSerialPort; private ListView mListView; private TextView mProgressBarTitle; private ProgressBar mProgressBar; private static final int MESSAGE_REFRESH = 101; private static final long REFRESH_TIMEOUT_MILLIS = 5000; + public static final String INTENT_ACTION_GRANT_USB = BuildConfig.APPLICATION_ID + ".GRANT_USB"; private final Handler mHandler = new Handler() { @Override @@ -81,6 +88,7 @@ public class DeviceListActivity extends Activity { } }; + private BroadcastReceiver mUsbReceiver; private List mEntries = new ArrayList(); private ArrayAdapter mAdapter; @@ -89,11 +97,25 @@ public class DeviceListActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); + final Context context = this; mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - mListView = (ListView) findViewById(R.id.deviceList); - mProgressBar = (ProgressBar) findViewById(R.id.progressBar); - mProgressBarTitle = (TextView) findViewById(R.id.progressBarTitle); + mListView = findViewById(R.id.deviceList); + mProgressBar = findViewById(R.id.progressBar); + mProgressBarTitle = findViewById(R.id.progressBarTitle); + + mUsbReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if(intent.getAction().equals(INTENT_ACTION_GRANT_USB)) { + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + showConsoleActivity(mSerialPort); + } else { + Toast.makeText(context, "USB permission denied", Toast.LENGTH_SHORT).show(); + } + } + } + }; mAdapter = new ArrayAdapter(this, android.R.layout.simple_expandable_list_item_2, mEntries) { @@ -135,8 +157,14 @@ public class DeviceListActivity extends Activity { return; } - final UsbSerialPort port = mEntries.get(position); - showConsoleActivity(port); + mSerialPort = mEntries.get(position); + UsbDevice device = mSerialPort.getDriver().getDevice(); + if (!mUsbManager.hasPermission(device)) { + PendingIntent usbPermissionIntent = PendingIntent.getBroadcast(context, 0, new Intent(INTENT_ACTION_GRANT_USB), 0); + mUsbManager.requestPermission(device, usbPermissionIntent); + } else { + showConsoleActivity(mSerialPort); + } } }); } @@ -145,12 +173,14 @@ public class DeviceListActivity extends Activity { protected void onResume() { super.onResume(); mHandler.sendEmptyMessage(MESSAGE_REFRESH); + registerReceiver(mUsbReceiver, new IntentFilter(INTENT_ACTION_GRANT_USB)); } @Override protected void onPause() { super.onPause(); mHandler.removeMessages(MESSAGE_REFRESH); + unregisterReceiver(mUsbReceiver); } private void refreshDeviceList() { From 37059b1a27dadb97359996ab623625480020b458 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Mon, 21 Oct 2019 21:04:58 +0200 Subject: [PATCH 16/20] reduce non covered code move HexDump class from library to example remove unused UsbSerialRuntimeException --- .../examples/DeviceListActivity.java | 5 +- .../hoho/android/usbserial/util/HexDump.java | 0 .../driver/UsbSerialRuntimeException.java | 46 ------------------- 3 files changed, 1 insertion(+), 50 deletions(-) rename {usbSerialForAndroid => usbSerialExamples}/src/main/java/com/hoho/android/usbserial/util/HexDump.java (100%) delete mode 100644 usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbSerialRuntimeException.java diff --git a/usbSerialExamples/src/main/java/com/hoho/android/usbserial/examples/DeviceListActivity.java b/usbSerialExamples/src/main/java/com/hoho/android/usbserial/examples/DeviceListActivity.java index 7b6fc0a..a69a3c0 100644 --- a/usbSerialExamples/src/main/java/com/hoho/android/usbserial/examples/DeviceListActivity.java +++ b/usbSerialExamples/src/main/java/com/hoho/android/usbserial/examples/DeviceListActivity.java @@ -49,7 +49,6 @@ import android.widget.TwoLineListItem; 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.HexDump; import java.util.ArrayList; import java.util.List; @@ -134,9 +133,7 @@ public class DeviceListActivity extends Activity { final UsbSerialDriver driver = port.getDriver(); final UsbDevice device = driver.getDevice(); - final String title = String.format("Vendor %s Product %s", - HexDump.toHexString((short) device.getVendorId()), - HexDump.toHexString((short) device.getProductId())); + final String title = String.format("Vendor %4X Product %4X", device.getVendorId(), device.getProductId()); row.getText1().setText(title); final String subtitle = driver.getClass().getSimpleName(); diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/util/HexDump.java b/usbSerialExamples/src/main/java/com/hoho/android/usbserial/util/HexDump.java similarity index 100% rename from usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/util/HexDump.java rename to usbSerialExamples/src/main/java/com/hoho/android/usbserial/util/HexDump.java diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbSerialRuntimeException.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbSerialRuntimeException.java deleted file mode 100644 index b48607c..0000000 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbSerialRuntimeException.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2011 Google Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, - * USA. - */ - -package com.hoho.android.usbserial.driver; - -/** - * Generic unchecked exception for the usbserial package. - * - * @author mike wakerly (opensource@hoho.com) - */ -@SuppressWarnings("serial") -public class UsbSerialRuntimeException extends RuntimeException { - - public UsbSerialRuntimeException() { - super(); - } - - public UsbSerialRuntimeException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public UsbSerialRuntimeException(String detailMessage) { - super(detailMessage); - } - - public UsbSerialRuntimeException(Throwable throwable) { - super(throwable); - } - -} From f7399c2aadff9199cedb66fc84784970b1c8b174 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Thu, 24 Oct 2019 22:25:36 +0200 Subject: [PATCH 17/20] more coverage tests purgeHwBuffers: adjust parameter names to match read/write methods and actual behavior --- .../hoho/android/usbserial/DeviceTest.java | 247 ++++++++++++++---- .../usbserial/driver/CdcAcmSerialDriver.java | 10 +- .../usbserial/driver/Ch34xSerialDriver.java | 5 - .../usbserial/driver/CommonUsbSerialPort.java | 4 +- .../usbserial/driver/Cp21xxSerialDriver.java | 5 +- .../usbserial/driver/FtdiSerialDriver.java | 14 +- .../driver/ProlificSerialDriver.java | 12 +- .../usbserial/driver/UsbSerialPort.java | 8 +- 8 files changed, 215 insertions(+), 90 deletions(-) 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 992d46a..f485fe4 100644 --- a/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java +++ b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java @@ -73,8 +73,9 @@ public class DeviceTest implements SerialInputOutputManager.Listener { private static int test_device_port; 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 int TELNET_COMMAND_WAIT = 2000; + 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"; @@ -83,13 +84,16 @@ public class DeviceTest implements SerialInputOutputManager.Listener { 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 final static byte RFC2217_PURGE_DATA = 12; private Context context; + private UsbManager usbManager; private UsbSerialDriver usbSerialDriver; private UsbDeviceConnection usbDeviceConnection; private UsbSerialPort usbSerialPort; private SerialInputOutputManager usbIoManager; private final Deque usbReadBuffer = new LinkedList<>(); + private Exception usbReadError; private boolean usbReadBlock = false; private long usbReadTime = 0; @@ -137,10 +141,18 @@ public class DeviceTest implements SerialInputOutputManager.Listener { setUpFixtureInt(); 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; + telnetClient.sendCommand((byte)TelnetCommand.SB); + telnetWriteStream.write(new byte[] {RFC2217_COM_PORT_OPTION, RFC2217_PURGE_DATA, 3}); + telnetClient.sendCommand((byte)TelnetCommand.SE); + for(int i=0; i availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager); assertEquals("no USB device found", 1, availableDrivers.size()); usbSerialDriver = availableDrivers.get(0); @@ -173,24 +185,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { } 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) { - @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) { - usbReadBuffer.clear(); - } + usbOpen(true); } @After @@ -266,6 +261,46 @@ public class DeviceTest implements SerialInputOutputManager.Listener { } } + private void usbClose() { + if (usbIoManager != null) { + usbIoManager.stop(); + usbIoManager = null; + } + if (usbSerialPort != null) { + try { + usbSerialPort.close(); + } catch (IOException ignored) { + } + usbSerialPort = null; + } + if(usbDeviceConnection != null) + usbDeviceConnection.close(); + usbDeviceConnection = null; + } + + private void usbOpen(boolean withIoManager) throws Exception { + usbDeviceConnection = usbManager.openDevice(usbSerialDriver.getDevice()); + usbSerialPort = usbSerialDriver.getPorts().get(test_device_port); + usbSerialPort.open(usbDeviceConnection); + usbSerialPort.setDTR(true); + usbSerialPort.setRTS(true); + if(withIoManager) { + 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) { + usbReadBuffer.clear(); + } + usbReadError = null; + } + // wait full time private byte[] usbRead() throws Exception { return usbRead(-1); @@ -276,6 +311,8 @@ public class DeviceTest implements SerialInputOutputManager.Listener { ByteBuffer buf = ByteBuffer.allocate(8192); if(usbIoManager != null) { while (System.currentTimeMillis() < end) { + if(usbReadError != null) + throw usbReadError; synchronized (usbReadBuffer) { while(usbReadBuffer.peek() != null) buf.put(usbReadBuffer.remove()); @@ -336,7 +373,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { 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++) { + for(int i=0; i " + data.length()); + + assertTrue(data.length() > 5); + if(purged) + assertTrue(data.length() < buf.length+1); + else + assertEquals(data.length(), buf.length + 3); + + // todo: purge receive buffer + } } diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java index a3d9a76..264bbd4 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java @@ -401,7 +401,7 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { public static Map getSupportedDevices() { final Map supportedDevices = new LinkedHashMap(); - supportedDevices.put(Integer.valueOf(UsbId.VENDOR_ARDUINO), + supportedDevices.put(UsbId.VENDOR_ARDUINO, new int[] { UsbId.ARDUINO_UNO, UsbId.ARDUINO_UNO_R3, @@ -414,19 +414,19 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { UsbId.ARDUINO_LEONARDO, UsbId.ARDUINO_MICRO, }); - supportedDevices.put(Integer.valueOf(UsbId.VENDOR_VAN_OOIJEN_TECH), + supportedDevices.put(UsbId.VENDOR_VAN_OOIJEN_TECH, new int[] { UsbId.VAN_OOIJEN_TECH_TEENSYDUINO_SERIAL, }); - supportedDevices.put(Integer.valueOf(UsbId.VENDOR_ATMEL), + supportedDevices.put(UsbId.VENDOR_ATMEL, new int[] { UsbId.ATMEL_LUFA_CDC_DEMO_APP, }); - supportedDevices.put(Integer.valueOf(UsbId.VENDOR_LEAFLABS), + supportedDevices.put(UsbId.VENDOR_LEAFLABS, new int[] { UsbId.LEAFLABS_MAPLE, }); - supportedDevices.put(Integer.valueOf(UsbId.VENDOR_ARM), + supportedDevices.put(UsbId.VENDOR_ARM, new int[] { UsbId.ARM_MBED, }); 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 0b0ae97..b35cbef 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 @@ -426,11 +426,6 @@ public class Ch34xSerialDriver implements UsbSerialDriver { writeHandshakeByte(); } - @Override - public boolean purgeHwBuffers(boolean purgeReadBuffers, boolean purgeWriteBuffers) throws IOException { - return true; - } - } public static Map getSupportedDevices() { diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CommonUsbSerialPort.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CommonUsbSerialPort.java index d47c5a9..fb7ddc9 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CommonUsbSerialPort.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CommonUsbSerialPort.java @@ -160,8 +160,8 @@ abstract class CommonUsbSerialPort implements UsbSerialPort { public abstract void setRTS(boolean value) throws IOException; @Override - public boolean purgeHwBuffers(boolean flushReadBuffers, boolean flushWriteBuffers) throws IOException { - return !flushReadBuffers && !flushWriteBuffers; + public boolean purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException { + return false; } } 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 4bc07ee..8bd90f0 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 @@ -379,8 +379,7 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { } @Override - public boolean purgeHwBuffers(boolean purgeReadBuffers, - boolean purgeWriteBuffers) throws IOException { + public boolean purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException { int value = (purgeReadBuffers ? FLUSH_READ_CODE : 0) | (purgeWriteBuffers ? FLUSH_WRITE_CODE : 0); @@ -395,7 +394,7 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { public static Map getSupportedDevices() { final Map supportedDevices = new LinkedHashMap(); - supportedDevices.put(Integer.valueOf(UsbId.VENDOR_SILABS), + supportedDevices.put(UsbId.VENDOR_SILABS, new int[] { UsbId.SILABS_CP2102, UsbId.SILABS_CP2105, 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 ccdc25e..60d0632 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 @@ -165,7 +165,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { private static final int SIO_SET_DATA_REQUEST = 4; private static final int SIO_RESET_SIO = 0; - private static final int SIO_RESET_PURGE_RX = 1; + private static final int SIO_RESET_PURGE_RX = 1; // RX @ FTDI device = write @ usb-serial-for-android library private static final int SIO_RESET_PURGE_TX = 2; public static final int FTDI_DEVICE_OUT_REQTYPE = @@ -534,20 +534,20 @@ public class FtdiSerialDriver implements UsbSerialDriver { } @Override - public boolean purgeHwBuffers(boolean purgeReadBuffers, boolean purgeWriteBuffers) throws IOException { - if (purgeReadBuffers) { + public boolean purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException { + if (purgeWriteBuffers) { int result = mConnection.controlTransfer(FTDI_DEVICE_OUT_REQTYPE, SIO_RESET_REQUEST, SIO_RESET_PURGE_RX, mIndex, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { - throw new IOException("Flushing RX failed: result=" + result); + throw new IOException("purge write buffer failed: result=" + result); } } - if (purgeWriteBuffers) { + if (purgeReadBuffers) { int result = mConnection.controlTransfer(FTDI_DEVICE_OUT_REQTYPE, SIO_RESET_REQUEST, SIO_RESET_PURGE_TX, mIndex, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { - throw new IOException("Flushing RX failed: result=" + result); + throw new IOException("purge read buffer failed: result=" + result); } } return true; @@ -556,7 +556,7 @@ public class FtdiSerialDriver implements UsbSerialDriver { public static Map getSupportedDevices() { final Map supportedDevices = new LinkedHashMap(); - supportedDevices.put(Integer.valueOf(UsbId.VENDOR_FTDI), + supportedDevices.put(UsbId.VENDOR_FTDI, new int[] { UsbId.FTDI_FT232R, UsbId.FTDI_FT232H, 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 c3c8b23..24ae76e 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 @@ -88,7 +88,7 @@ public class ProlificSerialDriver implements UsbSerialDriver { private static final int READ_ENDPOINT = 0x83; private static final int INTERRUPT_ENDPOINT = 0x81; - private static final int FLUSH_RX_REQUEST = 0x08; + private static final int FLUSH_RX_REQUEST = 0x08; // RX @ Prolific device = write @ usb-serial-for-android library private static final int FLUSH_TX_REQUEST = 0x09; private static final int SET_LINE_REQUEST = 0x20; @@ -553,22 +553,22 @@ public class ProlificSerialDriver implements UsbSerialDriver { } @Override - public boolean purgeHwBuffers(boolean purgeReadBuffers, boolean purgeWriteBuffers) throws IOException { - if (purgeReadBuffers) { + public boolean purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException { + if (purgeWriteBuffers) { vendorOut(FLUSH_RX_REQUEST, 0, null); } - if (purgeWriteBuffers) { + if (purgeReadBuffers) { vendorOut(FLUSH_TX_REQUEST, 0, null); } - return purgeReadBuffers || purgeWriteBuffers; + return true; } } public static Map getSupportedDevices() { final Map supportedDevices = new LinkedHashMap(); - supportedDevices.put(Integer.valueOf(UsbId.VENDOR_PROLIFIC), + supportedDevices.put(UsbId.VENDOR_PROLIFIC, new int[] { UsbId.PROLIFIC_PL2303, }); return supportedDevices; } diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbSerialPort.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbSerialPort.java index 1341143..5cd3ca5 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbSerialPort.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/UsbSerialPort.java @@ -216,13 +216,13 @@ public interface UsbSerialPort { public void setRTS(boolean value) throws IOException; /** - * Flush non-transmitted output data and / or non-read input data - * @param flushRX {@code true} to flush non-transmitted output data - * @param flushTX {@code true} to flush non-read input data + * purge non-transmitted output data and / or non-read input data + * @param purgeWriteBuffers {@code true} to discard non-transmitted output data + * @param purgeReadBuffers {@code true} to discard non-read input data * @return {@code true} if the operation was successful, or * {@code false} if the operation is not supported by the driver or device * @throws IOException if an error occurred during flush */ - public boolean purgeHwBuffers(boolean flushRX, boolean flushTX) throws IOException; + public boolean purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException; } From fac8c9f340937bfc6a2c14b5cf4c66e48a70b37f Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sat, 26 Oct 2019 15:40:40 +0200 Subject: [PATCH 18/20] test nonstandard baud rates --- usbSerialForAndroid/build.gradle | 7 +-- .../hoho/android/usbserial/DeviceTest.java | 61 +++++++++++++------ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/usbSerialForAndroid/build.gradle b/usbSerialForAndroid/build.gradle index c943135..5aa0170 100644 --- a/usbSerialForAndroid/build.gradle +++ b/usbSerialForAndroid/build.gradle @@ -8,10 +8,9 @@ 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 + testInstrumentationRunnerArguments = [ // Raspi Windows LinuxVM ... + 'rfc2217_server_host': '192.168.0.100', + 'rfc2217_server_nonstandard_baudrates': 'true', // true false false ] } 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 f485fe4..0ac13b9 100644 --- a/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java +++ b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java @@ -68,7 +68,6 @@ public class DeviceTest implements SerialInputOutputManager.Listener { 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; @@ -108,7 +107,6 @@ public class DeviceTest implements SerialInputOutputManager.Listener { 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")); @@ -532,8 +530,6 @@ public class DeviceTest implements SerialInputOutputManager.Listener { @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 @@ -604,9 +600,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { } catch (IllegalArgumentException ignored) { } - for(int baudRate : new int[] {300, 2400, 19200, 42000, 115200} ) { - if(baudRate == 42000 && !rfc2217_server_nonstandard_baudrates) - continue; // rfc2217_server.py would terminate + for(int baudRate : new int[] {300, 2400, 19200, 115200} ) { if(baudRate == 300 && isCp21xxRestrictedPort) { try { usbParameters(baudRate, 8, 1, UsbSerialPort.PARITY_NONE); @@ -620,10 +614,42 @@ public class DeviceTest implements SerialInputOutputManager.Listener { doReadWrite(baudRate+"/8N1"); } + if(rfc2217_server_nonstandard_baudrates && !isCp21xxRestrictedPort) { + // usbParameters does not fail on devices that do not support nonstandard baud rates + usbParameters(42000, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(42000, 8, 1, UsbSerialPort.PARITY_NONE); + + byte[] buf1 = "abc".getBytes(); + byte[] buf2 = "ABC".getBytes(); + byte[] data1, data2; + usbWrite(buf1); + data1 = telnetRead(); + telnetWrite(buf2); + data2 = usbRead(); + if (usbSerialDriver instanceof ProlificSerialDriver) { + // not supported + assertNotEquals(data1, buf2); + assertNotEquals(data2, buf2); + } else if (usbSerialDriver instanceof Cp21xxSerialDriver) { + if (usbSerialDriver.getPorts().size() > 1) { + // supported on cp2105 first port + assertThat("42000/8N1", data1, equalTo(buf1)); + assertThat("42000/8N1", data2, equalTo(buf2)); + } else { + // not supported on cp2102 + assertNotEquals(data1, buf1); + assertNotEquals(data2, buf2); + } + assertThat("42000/8N1", data1, equalTo(buf1)); + } else { + assertThat("42000/8N1", data2, equalTo(buf2)); + } + } { // non matching baud rate telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); usbParameters(2400, 8, 1, UsbSerialPort.PARITY_NONE); + byte[] data; telnetWrite("net2usb".getBytes()); data = usbRead(); assertNotEquals(7, data.length); @@ -762,7 +788,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { if (usbSerialDriver instanceof CdcAcmSerialDriver) { // not supported by arduino_leonardo_bridge.ino, other devices might support it - } else if (rfc2217_server_parity_mark_space) { + } else { usbParameters(19200, 7, 1, UsbSerialPort.PARITY_MARK); usbWrite(_8n1); data = telnetRead(4); @@ -794,17 +820,16 @@ public class DeviceTest implements SerialInputOutputManager.Listener { if (usbSerialDriver instanceof CdcAcmSerialDriver) { // not supported by arduino_leonardo_bridge.ino, other devices might support it } else { - 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_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)); - 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); From 800381e370a0ed5f73410c4a65961a0ed1c4c9d4 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sat, 26 Oct 2019 20:37:06 +0200 Subject: [PATCH 19/20] CdcAcm driver: cancel read() on close() --- .../android/usbserial/driver/CdcAcmSerialDriver.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java index 264bbd4..1e02232 100644 --- a/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java +++ b/usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CdcAcmSerialDriver.java @@ -50,6 +50,7 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { private final UsbDevice mDevice; private final UsbSerialPort mPort; + private UsbRequest mUsbRequest; public CdcAcmSerialDriver(UsbDevice device) { mDevice = device; @@ -253,6 +254,10 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { if (mConnection == null) { throw new IOException("Already closed"); } + synchronized (this) { + if (mUsbRequest != null) + mUsbRequest.cancel(); + } mConnection.close(); mConnection = null; } @@ -266,8 +271,11 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { if (!request.queue(buf, dest.length)) { throw new IOException("Error queueing request."); } - + mUsbRequest = request; final UsbRequest response = mConnection.requestWait(); + synchronized (this) { + mUsbRequest = null; + } if (response == null) { throw new IOException("Null response"); } @@ -280,6 +288,7 @@ public class CdcAcmSerialDriver implements UsbSerialDriver { return 0; } } finally { + mUsbRequest = null; request.close(); } } From b3631dff583197c6c6164f9c8a864078bf3add20 Mon Sep 17 00:00:00 2001 From: Kai Morich Date: Sat, 26 Oct 2019 21:32:28 +0200 Subject: [PATCH 20/20] README with SerialInputOutputManager for read() and port for write() test the usually not used read/write variants --- README.md | 87 ++++++------ test/serial_test/serial_test.ino | 43 ------ .../hoho/android/usbserial/DeviceTest.java | 124 +++++++++++++++--- .../util/SerialInputOutputManager.java | 4 +- 4 files changed, 152 insertions(+), 106 deletions(-) delete mode 100644 test/serial_test/serial_test.ino diff --git a/README.md b/README.md index 5037cc6..4b2b32b 100644 --- a/README.md +++ b/README.md @@ -33,66 +33,67 @@ dependencies { } ``` -**2.** Copy [device_filter.xml](https://github.com/mik3y/usb-serial-for-android/blob/master/usbSerialExamples/src/main/res/xml/device_filter.xml) to your project's `res/xml/` directory. - -**3.** Configure your `AndroidManifest.xml` to notify your app when a device is attached (see [Android USB Host documentation](http://developer.android.com/guide/topics/connectivity/usb/host.html#discovering-d) for help). +**2.** If the app should be notified when a device is attached, add +[device_filter.xml](https://github.com/mik3y/usb-serial-for-android/blob/master/usbSerialExamples/src/main/res/xml/device_filter.xml) +to your project's `res/xml/` directory and configure in your `AndroidManifest.xml`. ```xml - - - - + + + + ``` -**4.** Use it! Example code snippet: +**3.** Use it! Example code snippet: ```java -// Find all available drivers from attached devices. -UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); -List availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager); -if (availableDrivers.isEmpty()) { - return; -} + // Find all available drivers from attached devices. + UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); + List availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager); + if (availableDrivers.isEmpty()) { + return; + } -// Open a connection to the first available driver. -UsbSerialDriver driver = availableDrivers.get(0); -UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); -if (connection == null) { - // You probably need to call UsbManager.requestPermission(driver.getDevice(), ..) - return; -} + // Open a connection to the first available driver. + UsbSerialDriver driver = availableDrivers.get(0); + UsbDeviceConnection connection = manager.openDevice(driver.getDevice()); + if (connection == null) { + // add UsbManager.requestPermission(driver.getDevice(), ..) handling here + return; + } -// Read some data! Most have just one port (port 0). -UsbSerialPort port = driver.getPorts().get(0); -try { - port.open(connection); - port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); - - byte buffer[] = new byte[16]; - int numBytesRead = port.read(buffer, 1000); - Log.d(TAG, "Read " + numBytesRead + " bytes."); -} catch (IOException e) { - // Deal with error. -} finally { - port.close(); + UsbSerialPort port = driver.getPorts().get(0); // Most devices have just one port (port 0) + port.open(connection); + port.setParameters(115200, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); + usbIoManager = new SerialInputOutputManager(usbSerialPort, this); + Executors.newSingleThreadExecutor().submit(usbIoManager); +``` +```java + port.write("hello".getBytes(), WRITE_WAIT_MILLIS); +``` +```java +@Override +public void onNewData(byte[] data) { + runOnUiThread(() -> { textView.append(new String(data)); }); } ``` +```java + port.close(); +``` -For a simple example, see the -[UsbSerialExamples project](https://github.com/mik3y/usb-serial-for-android/blob/master/usbSerialExamples) -in git, which is a simple application for reading and showing serial data. + +For a simple example, see +[UsbSerialExamples](https://github.com/mik3y/usb-serial-for-android/blob/master/usbSerialExamples) +folder in this project. For a more complete example, see separate github project -[SimpleUsbTerminal](https://github.com/kai-morich/SimpleUsbTerminal) - -A [simple Arduino application](https://github.com/mik3y/usb-serial-for-android/blob/master/arduino) -is also available which can be used for testing. +[SimpleUsbTerminal](https://github.com/kai-morich/SimpleUsbTerminal). ## Probing for Unrecognized Devices diff --git a/test/serial_test/serial_test.ino b/test/serial_test/serial_test.ino deleted file mode 100644 index 6964001..0000000 --- a/test/serial_test/serial_test.ino +++ /dev/null @@ -1,43 +0,0 @@ -/* Copyright 2012 Google Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, - * USA. - * - * Project home page: http://code.google.com/p/usb-serial-for-android/ - */ - -// Sample Arduino sketch for use with usb-serial-for-android. -// Prints an ever-increasing counter, and writes back anything -// it receives. - -static int counter = 0; -void setup() { - Serial.begin(115200); -} - -void loop() { - Serial.print("Tick #"); - Serial.print(counter++, DEC); - Serial.print("\n"); - - if (Serial.peek() != -1) { - Serial.print("Read: "); - do { - Serial.print((char) Serial.read()); - } while (Serial.peek() != -1); - Serial.print("\n"); - } - delay(1000); -} 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 0ac13b9..4d92811 100644 --- a/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java +++ b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java @@ -194,22 +194,7 @@ public class DeviceTest implements SerialInputOutputManager.Listener { 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; + usbClose(); usbSerialDriver = null; } @@ -261,10 +246,15 @@ public class DeviceTest implements SerialInputOutputManager.Listener { private void usbClose() { if (usbIoManager != null) { + usbIoManager.setListener(null); usbIoManager.stop(); - usbIoManager = null; } if (usbSerialPort != null) { + try { + usbSerialPort.setDTR(false); + usbSerialPort.setRTS(false); + } catch (Exception ignored) { + } try { usbSerialPort.close(); } catch (IOException ignored) { @@ -274,6 +264,18 @@ public class DeviceTest implements SerialInputOutputManager.Listener { if(usbDeviceConnection != null) usbDeviceConnection.close(); usbDeviceConnection = null; + if(usbIoManager != null) { + for(int i=0; i<2000; i++) { + if(SerialInputOutputManager.State.STOPPED == usbIoManager.getState()) break; + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + assertEquals(SerialInputOutputManager.State.STOPPED, usbIoManager.getState()); + usbIoManager = null; + } } private void usbOpen(boolean withIoManager) throws Exception { @@ -520,6 +522,11 @@ public class DeviceTest implements SerialInputOutputManager.Listener { usbSerialPort.close(); } catch (IOException ignored) { } + + if (usbSerialDriver instanceof Cp21xxSerialDriver) { // why needed? + usbIoManager.stop(); + usbIoManager = null; + } // full re-open supported usbClose(); usbOpen(true); @@ -640,8 +647,8 @@ public class DeviceTest implements SerialInputOutputManager.Listener { assertNotEquals(data1, buf1); assertNotEquals(data2, buf2); } - assertThat("42000/8N1", data1, equalTo(buf1)); } else { + assertThat("42000/8N1", data1, equalTo(buf1)); assertThat("42000/8N1", data2, equalTo(buf2)); } } @@ -1109,4 +1116,85 @@ public class DeviceTest implements SerialInputOutputManager.Listener { // todo: purge receive buffer } + + @Test + // WriteAsync rarely makes sense, as data is not written until something is read + public void writeAsync() throws Exception { + if (usbSerialDriver instanceof FtdiSerialDriver) + return; // periodically sends status messages, so does not block here + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + + SerialInputOutputManager ioManager; + ioManager = new SerialInputOutputManager(null); + assertEquals(null, ioManager.getListener()); + ioManager.setListener(this); + assertEquals(this, ioManager.getListener()); + ioManager = new SerialInputOutputManager(null, this); + assertEquals(this, ioManager.getListener()); + + byte[] data, buf = new byte[]{1}; + int len; + usbIoManager.writeAsync(buf); + usbIoManager.writeAsync(buf); + data = telnetRead(1); + assertEquals(0, data.length); + telnetWrite(buf); + data = usbRead(1); + assertEquals(1, data.length); + data = telnetRead(2); + assertEquals(2, data.length); + } + + @Test + // Blocking read should be avoided in the UI thread, as it makes the app unresponsive. + // You better use the SerialInputOutputManager. + // + // With the change from bulkTransfer to queued requests, the read timeout has no effect + // and the call blocks until close() if no data is available! + // The change from bulkTransfer to queued requests was necessary to prevent data loss. + public void readSync() throws Exception { + if (usbSerialDriver instanceof FtdiSerialDriver) + return; // periodically sends status messages, so does not block here + + Runnable closeThread = new Runnable() { + @Override + public void run() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + usbClose(); + } + }; + + usbClose(); + usbOpen(false); + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + + byte[] buf = new byte[]{1}; + int len; + long time; + telnetWrite(buf); + len = usbSerialPort.read(buf, 0); // not blocking because data is available + assertEquals(1, len); + + time = System.currentTimeMillis(); + Executors.newSingleThreadExecutor().submit(closeThread); + len = usbSerialPort.read(buf, 0); // blocking until close() + assertEquals(0, len); + assertTrue(System.currentTimeMillis()-time >= 100); + + usbOpen(false); + usbParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + telnetParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE); + + time = System.currentTimeMillis(); + Executors.newSingleThreadExecutor().submit(closeThread); + len = usbSerialPort.read(buf, 10); // timeout not used any more -> blocking until close() + assertEquals(0, len); + assertTrue(System.currentTimeMillis()-time >= 100); + } } 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 78bcd0b..1ab3edf 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 @@ -50,7 +50,7 @@ public class SerialInputOutputManager implements Runnable { // Synchronized by 'mWriteBuffer' private final ByteBuffer mWriteBuffer = ByteBuffer.allocate(BUFSIZ); - private enum State { + public enum State { STOPPED, RUNNING, STOPPING @@ -111,7 +111,7 @@ public class SerialInputOutputManager implements Runnable { } } - private synchronized State getState() { + public synchronized State getState() { return mState; }