Skip to content

Commit 90246e5

Browse files
committed
setReadQueue(...), enabled by default in SerialInputOutputManager
for applications doing permanent read() with timeout=0, multiple buffers can be used to copy next data from Linux kernel, while the current data is processed.
1 parent 026355f commit 90246e5

File tree

7 files changed

+312
-23
lines changed

7 files changed

+312
-23
lines changed

usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/DeviceTest.java

+64-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import android.hardware.usb.UsbDevice;
1515
import android.hardware.usb.UsbDeviceConnection;
1616
import android.hardware.usb.UsbManager;
17+
import android.hardware.usb.UsbRequest;
1718
import android.os.Process;
1819
import androidx.test.core.app.ApplicationProvider;
1920
import androidx.test.platform.app.InstrumentationRegistry;
@@ -43,7 +44,6 @@
4344
import com.hoho.android.usbserial.driver.UsbSerialPort.FlowControl;
4445
import com.hoho.android.usbserial.util.XonXoffFilter;
4546

46-
4747
import org.junit.After;
4848
import org.junit.AfterClass;
4949
import org.junit.Assume;
@@ -60,11 +60,13 @@
6060
import java.nio.BufferOverflowException;
6161
import java.util.Arrays;
6262
import java.util.EnumSet;
63+
import java.util.LinkedList;
6364
import java.util.List;
6465
import java.util.concurrent.Executors;
6566
import java.util.concurrent.ScheduledExecutorService;
6667
import java.util.concurrent.ScheduledFuture;
6768
import java.util.concurrent.TimeUnit;
69+
import java.util.stream.Collectors;
6870

6971
import static org.hamcrest.CoreMatchers.anyOf;
7072
import static org.hamcrest.MatcherAssert.assertThat;
@@ -1148,7 +1150,7 @@ else if (usb.serialDriver instanceof ProlificSerialDriver)
11481150
public void readBufferOverflow() throws Exception {
11491151
if(usb.serialDriver instanceof CdcAcmSerialDriver)
11501152
telnet.writeDelay = 10; // arduino_leonardo_bridge.ino sends each byte in own USB packet, which is horribly slow
1151-
usb.open();
1153+
usb.open(EnumSet.of(UsbWrapper.OpenCloseFlags.NO_IOMANAGER_READQUEUE));
11521154
usb.setParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE);
11531155
telnet.setParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE);
11541156

@@ -1198,6 +1200,64 @@ public void readBufferOverflow() throws Exception {
11981200
assertTrue(data.length() != expected.length());
11991201
}
12001202

1203+
@Test
1204+
public void readQueue() throws Exception {
1205+
class CountingUsbRequest extends UsbRequest {
1206+
int count;
1207+
@Override public Object getClientData() { count += 1; return super.getClientData(); }
1208+
}
1209+
1210+
usb.open(EnumSet.of(UsbWrapper.OpenCloseFlags.NO_IOMANAGER_THREAD));
1211+
int len = usb.serialPort.getReadEndpoint().getMaxPacketSize();
1212+
usb.close();
1213+
CommonUsbSerialPortWrapper.setReadQueueRequestSupplier(usb.serialPort, CountingUsbRequest::new);
1214+
CommonUsbSerialPort port = (CommonUsbSerialPort) usb.serialPort;
1215+
1216+
port.setReadQueue(2, len);
1217+
usb.open(EnumSet.of(UsbWrapper.OpenCloseFlags.NO_IOMANAGER_START));
1218+
usb.setParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE);
1219+
telnet.setParameters(115200, 8, 1, UsbSerialPort.PARITY_NONE);
1220+
assertEquals(2, port.getReadQueueBufferCount());
1221+
assertEquals(4, usb.ioManager.getReadQueueBufferCount()); // not set at port yet
1222+
assertThrows(IllegalStateException.class, () -> usb.ioManager.setReadQueue(1)); // cannot reduce bufferCount
1223+
usb.ioManager.setReadQueue(2);
1224+
usb.ioManager.start();
1225+
port.setReadQueue(4, len);
1226+
1227+
// linux kernel does round-robin
1228+
LinkedList<UsbRequest> requests = CommonUsbSerialPortWrapper.getReadQueueRequests(usb.serialPort);
1229+
assertNotNull(requests);
1230+
for (int i=0; i<16; i++) {
1231+
telnet.write(new byte[1]);
1232+
usb.read(1);
1233+
}
1234+
List<Integer> requestCounts;
1235+
if(usb.serialDriver instanceof FtdiSerialDriver) {
1236+
for (UsbRequest request : requests) {
1237+
int count = ((CountingUsbRequest)request).count;
1238+
assertTrue(String.valueOf(count), count >= 4);
1239+
}
1240+
} else {
1241+
requestCounts = requests.stream().map(r -> ((CountingUsbRequest)r).count).collect(Collectors.toList());
1242+
assertThat(requestCounts, equalTo(Arrays.asList(4, 4, 4, 4)));
1243+
}
1244+
usb.ioManager.setReadQueue(6);
1245+
for (int i=0; i<18; i++) {
1246+
telnet.write(new byte[1]);
1247+
usb.read(1);
1248+
}
1249+
requestCounts = requests.stream().map(r -> ((CountingUsbRequest)r).count).collect(Collectors.toList());
1250+
if(!(usb.serialDriver instanceof FtdiSerialDriver)) {
1251+
assertThat(requestCounts, equalTo(Arrays.asList(7, 7, 7, 7, 3, 3)));
1252+
}
1253+
usb.close();
1254+
usb.open(EnumSet.of(UsbWrapper.OpenCloseFlags.NO_IOMANAGER_START));
1255+
port.setReadQueue(8, len);
1256+
assertThrows(IllegalStateException.class, () -> usb.serialPort.read(new byte[len], 1) ); // cannot use timeout != 0
1257+
assertThrows(IllegalStateException.class, () -> usb.serialPort.read(new byte[4], 0) ); // cannot use different length
1258+
assertThrows(IllegalStateException.class, () -> usb.ioManager.start()); // cannot reduce bufferCount
1259+
}
1260+
12011261
@Test
12021262
public void readSpeed() throws Exception {
12031263
// see logcat for performance results
@@ -1223,7 +1283,7 @@ private int readSpeedInt(int writeSeconds, int readBufferSize, int readTimeout)
12231283
if(usb.serialDriver instanceof CdcAcmSerialDriver)
12241284
writeAhead = 50;
12251285

1226-
usb.open(EnumSet.of(UsbWrapper.OpenCloseFlags.NO_IOMANAGER_START));
1286+
usb.open(EnumSet.of(UsbWrapper.OpenCloseFlags.NO_IOMANAGER_START, UsbWrapper.OpenCloseFlags.NO_IOMANAGER_READQUEUE));
12271287
usb.ioManager.setReadTimeout(readTimeout);
12281288
if(readBufferSize > 0)
12291289
usb.ioManager.setReadBufferSize(readBufferSize);
@@ -1421,7 +1481,7 @@ public void IoManager() throws Exception {
14211481
usb.ioManager.setWriteTimeout(usb.ioManager.getWriteTimeout());
14221482
usb.close();
14231483

1424-
usb.open(EnumSet.of(UsbWrapper.OpenCloseFlags.NO_IOMANAGER_START)); // creates new IoManager
1484+
usb.open(EnumSet.of(UsbWrapper.OpenCloseFlags.NO_IOMANAGER_START, UsbWrapper.OpenCloseFlags.NO_IOMANAGER_READQUEUE)); // creates new IoManager
14251485
usb.setParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE);
14261486
telnet.setParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE);
14271487
usb.ioManager.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
package com.hoho.android.usbserial.driver;
22

3+
import android.hardware.usb.UsbRequest;
4+
5+
import com.hoho.android.usbserial.util.UsbUtils;
6+
7+
import java.util.LinkedList;
8+
39
public class CommonUsbSerialPortWrapper {
410
public static byte[] getWriteBuffer(UsbSerialPort serialPort) {
511
CommonUsbSerialPort commonSerialPort = (CommonUsbSerialPort) serialPort;
612
return commonSerialPort.mWriteBuffer;
713
}
14+
15+
public static LinkedList<UsbRequest> getReadQueueRequests(UsbSerialPort serialPort) {
16+
CommonUsbSerialPort commonSerialPort = (CommonUsbSerialPort) serialPort;
17+
return commonSerialPort.mReadQueueRequests;
18+
}
19+
20+
public static void setReadQueueRequestSupplier(UsbSerialPort serialPort, UsbUtils.Supplier<UsbRequest> supplier) {
21+
CommonUsbSerialPort commonSerialPort = (CommonUsbSerialPort) serialPort;
22+
commonSerialPort.mUsbRequestSupplier = supplier;
23+
}
824
}

usbSerialForAndroid/src/androidTest/java/com/hoho/android/usbserial/util/UsbWrapper.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public class UsbWrapper implements SerialInputOutputManager.Listener {
4141
public final static int USB_WRITE_WAIT = 500;
4242
private static final String TAG = UsbWrapper.class.getSimpleName();
4343

44-
public enum OpenCloseFlags { NO_IOMANAGER_THREAD, NO_IOMANAGER_START, NO_CONTROL_LINE_INIT, NO_DEVICE_CONNECTION };
44+
public enum OpenCloseFlags { NO_IOMANAGER_THREAD, NO_IOMANAGER_READQUEUE, NO_IOMANAGER_START, NO_CONTROL_LINE_INIT, NO_DEVICE_CONNECTION };
4545

4646
// constructor
4747
final Context context;
@@ -198,7 +198,7 @@ public void close(EnumSet<OpenCloseFlags> flags) {
198198
serialPort.close();
199199
} catch (Exception ignored) {
200200
}
201-
//usbSerialPort = null;
201+
((CommonUsbSerialPort)serialPort).setReadQueue(0, 0);
202202
}
203203
if(!flags.contains(OpenCloseFlags.NO_DEVICE_CONNECTION)) {
204204
deviceConnection = null; // closed in usbSerialPort.close()
@@ -233,6 +233,8 @@ public void open(EnumSet<OpenCloseFlags> flags) throws Exception {
233233
}
234234
if(!flags.contains(OpenCloseFlags.NO_IOMANAGER_THREAD)) {
235235
ioManager = new SerialInputOutputManager(serialPort, this);
236+
if(flags.contains(OpenCloseFlags.NO_IOMANAGER_READQUEUE))
237+
ioManager.setReadQueue(0);
236238
if(!flags.contains(OpenCloseFlags.NO_IOMANAGER_START))
237239
ioManager.start();
238240
}

usbSerialForAndroid/src/main/java/com/hoho/android/usbserial/driver/CommonUsbSerialPort.java

+91-12
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
import android.util.Log;
1515

1616
import com.hoho.android.usbserial.util.MonotonicClock;
17+
import com.hoho.android.usbserial.util.UsbUtils;
1718

1819
import java.io.IOException;
1920
import java.nio.ByteBuffer;
2021
import java.util.EnumSet;
22+
import java.util.LinkedList;
23+
import java.util.Objects;
2124

2225
/**
2326
* A base class shared by several driver implementations.
@@ -38,8 +41,12 @@ public abstract class CommonUsbSerialPort implements UsbSerialPort {
3841
protected UsbDeviceConnection mConnection;
3942
protected UsbEndpoint mReadEndpoint;
4043
protected UsbEndpoint mWriteEndpoint;
41-
protected UsbRequest mUsbRequest;
44+
protected UsbRequest mReadRequest;
45+
protected LinkedList<UsbRequest> mReadQueueRequests;
46+
private int mReadQueueBufferCount;
47+
private int mReadQueueBufferSize;
4248
protected FlowControl mFlowControl = FlowControl.NONE;
49+
protected UsbUtils.Supplier<UsbRequest> mUsbRequestSupplier = UsbRequest::new; // override for testing
4350

4451
/**
4552
* Internal write buffer.
@@ -110,6 +117,50 @@ public final void setWriteBufferSize(int bufferSize) {
110117
}
111118
}
112119

120+
/**
121+
* for applications doing permanent read() with timeout=0, multiple buffers can be
122+
* used to copy next data from Linux kernel, while the current data is processed.
123+
* @param bufferCount number of buffers to use for readQueue
124+
* disabled with 0
125+
* @param bufferSize size of each buffer
126+
*/
127+
public void setReadQueue(int bufferCount, int bufferSize) {
128+
if (bufferCount < 0) {
129+
throw new IllegalArgumentException("Invalid bufferCount");
130+
}
131+
if (bufferCount > 0 && bufferSize <= 0) {
132+
throw new IllegalArgumentException("Invalid bufferSize");
133+
}
134+
if(isOpen()) {
135+
if (bufferCount < mReadQueueBufferCount) {
136+
throw new IllegalStateException("Cannot reduce bufferCount when port is open");
137+
}
138+
if (mReadQueueBufferCount != 0 && bufferSize != mReadQueueBufferSize) {
139+
throw new IllegalStateException("Cannot change bufferSize when port is open");
140+
}
141+
if (bufferCount > 0) {
142+
if (mReadQueueRequests == null) {
143+
mReadQueueRequests = new LinkedList<>();
144+
}
145+
for (int i = mReadQueueRequests.size(); i < bufferCount; i++) {
146+
ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
147+
UsbRequest request = mUsbRequestSupplier.get();
148+
request.initialize(mConnection, mReadEndpoint);
149+
request.setClientData(buffer);
150+
request.queue(buffer, bufferSize);
151+
mReadQueueRequests.add(request);
152+
}
153+
}
154+
}
155+
mReadQueueBufferCount = bufferCount;
156+
mReadQueueBufferSize = bufferSize;
157+
}
158+
159+
public int getReadQueueBufferCount() { return mReadQueueBufferCount; }
160+
public int getReadQueueBufferSize() { return mReadQueueBufferSize; }
161+
162+
private boolean useReadQueue() { return mReadQueueBufferCount != 0; }
163+
113164
@Override
114165
public void open(UsbDeviceConnection connection) throws IOException {
115166
if (mConnection != null) {
@@ -125,8 +176,9 @@ public void open(UsbDeviceConnection connection) throws IOException {
125176
if (mReadEndpoint == null || mWriteEndpoint == null) {
126177
throw new IOException("Could not get read & write endpoints");
127178
}
128-
mUsbRequest = new UsbRequest();
129-
mUsbRequest.initialize(mConnection, mReadEndpoint);
179+
mReadRequest = mUsbRequestSupplier.get();
180+
mReadRequest.initialize(mConnection, mReadEndpoint);
181+
setReadQueue(mReadQueueBufferCount, mReadQueueBufferSize); // fill mReadQueueRequests
130182
ok = true;
131183
} finally {
132184
if (!ok) {
@@ -144,11 +196,19 @@ public void close() throws IOException {
144196
if (mConnection == null) {
145197
throw new IOException("Already closed");
146198
}
147-
UsbRequest usbRequest = mUsbRequest;
148-
mUsbRequest = null;
199+
UsbRequest readRequest = mReadRequest;
200+
mReadRequest = null;
149201
try {
150-
usbRequest.cancel();
202+
readRequest.cancel();
151203
} catch(Exception ignored) {}
204+
if(mReadQueueRequests != null) {
205+
for(UsbRequest readQueueRequest : mReadQueueRequests) {
206+
try {
207+
readQueueRequest.cancel();
208+
} catch(Exception ignored) {}
209+
}
210+
mReadQueueRequests = null;
211+
}
152212
try {
153213
closeInt();
154214
} catch(Exception ignored) {}
@@ -168,7 +228,7 @@ protected void testConnection(boolean full) throws IOException {
168228
}
169229

170230
protected void testConnection(boolean full, String msg) throws IOException {
171-
if(mUsbRequest == null) {
231+
if(mReadRequest == null) {
172232
throw new IOException("Connection closed");
173233
}
174234
if(!full) {
@@ -199,6 +259,9 @@ protected int read(final byte[] dest, int length, final int timeout, boolean tes
199259
length = Math.min(length, dest.length);
200260
final int nread;
201261
if (timeout != 0) {
262+
if(useReadQueue()) {
263+
throw new IllegalStateException("Cannot use timeout!=0 if readQueue is enabled");
264+
}
202265
// bulkTransfer will cause data loss with short timeout + high baud rates + continuous transfer
203266
// https://stackoverflow.com/questions/9108548/android-usb-host-bulktransfer-is-losing-data
204267
// but mConnection.requestWait(timeout) available since Android 8.0 es even worse,
@@ -216,15 +279,31 @@ protected int read(final byte[] dest, int length, final int timeout, boolean tes
216279
testConnection(MonotonicClock.millis() < endTime);
217280

218281
} else {
219-
final ByteBuffer buf = ByteBuffer.wrap(dest, 0, length);
220-
if (!mUsbRequest.queue(buf, length)) {
221-
throw new IOException("Queueing USB request failed");
282+
ByteBuffer buf = null;
283+
if(useReadQueue()) {
284+
if (length != mReadQueueBufferSize) {
285+
throw new IllegalStateException("Cannot use different length if readQueue is enabled");
286+
}
287+
} else {
288+
buf = ByteBuffer.wrap(dest, 0, length);
289+
if (!mReadRequest.queue(buf, length)) {
290+
throw new IOException("Queueing USB request failed");
291+
}
222292
}
223293
final UsbRequest response = mConnection.requestWait();
224294
if (response == null) {
225295
throw new IOException("Waiting for USB request failed");
226296
}
227-
nread = buf.position();
297+
if(useReadQueue()) {
298+
buf = (ByteBuffer) response.getClientData();
299+
System.arraycopy(buf.array(), 0, dest, 0, buf.position());
300+
if(mReadRequest != null) { // re-queue if connection not closed
301+
if (!response.queue(buf, buf.capacity())) {
302+
throw new IOException("Queueing USB request failed");
303+
}
304+
}
305+
}
306+
nread = Objects.requireNonNull(buf).position();
228307
// Android error propagation is improvable:
229308
// response != null & nread == 0 can be: connection lost, buffer to small, ???
230309
if(nread == 0) {
@@ -297,7 +376,7 @@ public void write(final byte[] src, int length, final int timeout) throws IOExce
297376

298377
@Override
299378
public boolean isOpen() {
300-
return mUsbRequest != null;
379+
return mReadRequest != null;
301380
}
302381

303382
@Override

0 commit comments

Comments
 (0)