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/.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 aaaf922..c0f68ed 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,38 +1,34 @@ - - + + + - - - - - - - - - - - + - - - - - 1.8 - - - - - - - \ No newline at end of file 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..df159ef 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.1.2' } } allprojects { repositories { - mavenCentral() + jcenter() + google() } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8c0fb64..13372ae 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f3d36d0..eb10a04 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 +#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.4-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..cde7b10 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 27 + buildToolsVersion '27.0.3' defaultConfig { minSdkVersion 14 - targetSdkVersion 22 + targetSdkVersion 27 - 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..d335842 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 27 + buildToolsVersion '27.0.3' defaultConfig { - minSdkVersion 12 - targetSdkVersion 19 + minSdkVersion 14 + targetSdkVersion 27 + 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.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' +} + 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..e20ab5e --- /dev/null +++ b/usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java @@ -0,0 +1,972 @@ +/* + * 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 android.os.Process; + +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.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 { + + // 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; + 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; + 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 long usbReadTime = 0; + + private static TelnetClient telnetClient; + private static InputStream telnetReadStream; + 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 { + 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 correctly 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); + 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}; + 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) { + @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(); + } + } + + @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(8192); + 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) { + 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); + } catch (InterruptedException e) { + e.printStackTrace(); + } + synchronized (usbReadBuffer) { + usbReadBuffer.add(data); + } + } + + @Override + public void onRunError(Exception e) { + 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 { + 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.io.IOException e) { // cp2105 second port + } 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.io.IOException e) { // cp2105 second port + } 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.io.IOException e) { // cp2105 second port + } catch (java.lang.IllegalArgumentException e) { + } + + 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); + + 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 + 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}); + 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 (!(isCp21xxRestrictedPort || 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 (!(isCp21xxRestrictedPort || 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) { + } + } + 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); + 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 if (rfc2217_server_parity_mark_space) { + 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 { + 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); + 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 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[]{(byte)0x41, (byte)0xf1}); + data = telnetRead(2); + assertThat("19200/8N1", data, equalTo(new byte[]{1, 5})); + + // out 8N2: addddddd dooaddddddddoo + // 1000001 0 10011111 + // in 6N1: addddddo addddddo + // 100000 110100 + 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 + } + } + + + @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("%07d,", 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("%07d,", 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 = 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()); + } + + + // + // 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 + 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) + writeAhead = 50; + + int linenr = 0; + String line=""; + StringBuilder data = new StringBuilder(); + StringBuilder expected = new StringBuilder(); + int dlen = 0, elen = 0; + Log.i(TAG, "readSpeed: 'read' should be near "+baudrate/10); + long begin = System.currentTimeMillis(); + long next = System.currentTimeMillis(); + for(int seconds=1; seconds <= writeSeconds; seconds++) { + next += 1000; + while (System.currentTimeMillis() < next) { + if((writeAhead < 0) || (expected.length() < data.length() + writeAhead)) { + line = String.format("%07d,", 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)+", read="+(data.length()-dlen)+", write="+(expected.length()-elen)); + 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); + } + 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 + 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 + // 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: '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,", linenr++); + else + line = String.format("%07d,", linenr++); + usbWrite(line.getBytes()); + expected.append(line); + data.append(new String(telnetRead(0))); + } + Log.i(TAG, "writeSpeed: t="+(next-begin)+", write="+(expected.length()-elen)+", read="+(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)+", read="+(data.length()-dlen)); + assertTrue(found); + 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())); + assertThat(datasub, equalTo(expectedsub)); + } + } + +} 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..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,25 +194,26 @@ public class Ch34xSerialDriver implements UsbSerialDriver { return 0; } } finally { + mUsbRequest = null; 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..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 @@ -26,10 +26,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.util.Collections; +import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -39,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 @@ -53,7 +59,7 @@ public class Cp21xxSerialDriver implements UsbSerialDriver { @Override public List getPorts() { - return Collections.singletonList(mPort); + return mPorts; } public class Cp21xxSerialPort extends CommonUsbSerialPort { @@ -101,11 +107,18 @@ 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; + + // 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); + mEnableAsyncReads = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1); } @Override @@ -115,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 @@ -126,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) { @@ -169,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(); @@ -179,21 +195,51 @@ 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."); + } + 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 { + mUsbRequest = null; + 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 @@ -238,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."); } @@ -252,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: @@ -277,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: @@ -292,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/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/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; 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() {