/* * 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<byte[]> 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<UsbSerialDriver> 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<UsbSerialPort> getPorts() { return null; } } List<UsbSerialDriver> 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)); } } }