文章目录
- usb协议
- 操作流程
- 如何收发字节数据
usb协议
可以简单认为host拥有一棵usb树,在这棵树上挂载usb节点。查询和连接指定的device时需要给出两个信息:
- vendorId
- productId
因此对于high-level api,有以下查找device的实现:
// 和递归查目录树是一样的
public UsbDevice findDevice(UsbHub hub, short vendorId, short productId)
{
for (UsbDevice device : (List<UsbDevice>) hub.getAttachedUsbDevices())
{
UsbDeviceDescriptor desc = device.getUsbDeviceDescriptor();
if (desc.idVendor() == vendorId && desc.idProduct() == productId) return device;
if (device.isUsbHub())
{
device = findDevice((UsbHub) device, vendorId, productId);
if (device != null) return device;
}
}
return null;
}
操作流程
使用usb4java的通信流程如下:
graph TD
A[find device]
A-->B[claim iface]
B-->C{get endpoint}
C-->|UsbConst.OUT| D[outPipe]
C-->|UsbConst.IN| E[inPipe]
D-->F[open pipe]
E-->F[open pipe]
F-->G[syn- or asyn- submit byte data]
G-->H[close pipe]
H-->I[release iface]
上述endpoint之后分出in和out两种pipe,与usb的位定义有关。具体地,endpoint有一个字节的描述符,其中0:3位表示usb number;4:6位保留,第7位指示是发送还是接收。因此通过指定第7位的值(使用UsbConst)来获取in/out pipe。
如何收发字节数据
当pipe打开后,有两种方式收发数据:同步和异步submit。以下主要介绍异步submit是如何收发数据的。
首先submit需要传入一个byte数组:
byte[] data = new byte[1024];
inPipe.asyncSubmit(data);
该数组会被UsbIrp对象包装,然后塞到一个IrpQueue中。
final UsbIrp irp = createUsbIrp();
irp.setAcceptShortPacket(true);
irp.setData(data);
...
this.queue.add(irp);
队列检测到有包进入,启用一个线程处理:
public final void add(final T irp)
{
this.irps.add(irp);
// Start the queue processor if not already running.
if (this.processor == null)
{
this.processor = new Thread(new Runnable()
{
@Override
public void run()
{
process();
}
});
this.processor.setDaemon(true);
this.processor.setName("usb4java IRP Queue Processor");
this.processor.start();
}
}
具体处理步骤如下:
final void process()
{
// Get the next IRP
T irp = this.irps.poll();
// If there are no IRPs to process then mark the thread as closing
// right away. Otherwise process the IRP (and more IRPs from the queue
// if present).
if (irp == null)
{
this.processor = null;
}
else
{
while (irp != null)
{
// Process the IRP
try
{
processIrp(irp);
}
catch (final UsbException e)
{
irp.setUsbException(e);
}
// Get next IRP and mark the thread as closing before sending
// the events for the previous IRP
final T nextIrp = this.irps.poll();
if (nextIrp == null) this.processor = null;
// Finish the previous IRP
irp.complete();
finishIrp(irp);
// Process next IRP (if present)
irp = nextIrp;
}
}
// No more IRPs are present in the queue so terminate the thread.
synchronized (this.irps)
{
this.irps.notifyAll();
}
}
可以看到,在处理一个Irp之后,线程会调用finish(irp)方法,该方法实际是在发送一个事件给监听者。
protected void finishIrp(final UsbIrp irp)
{
this.pipe.sendEvent(irp);
}
public void sendEvent(final UsbIrp irp)
{
if (irp.isUsbException())
{
this.listeners.errorEventOccurred(new UsbPipeErrorEvent(this, irp));
}
else
{
this.listeners.dataEventOccurred(new UsbPipeDataEvent(this, irp));
}
}
现在你就知道为什么官网的Guide上说使用异步submit要添加一个监听器了。
到这里,我们还想深入探究下process方法中的processIrp具体是怎么工作的,它是不是irp包没装满就返回呢?
protected void processIrp(final UsbIrp irp) throws UsbException
{
final UsbEndpoint endpoint = this.pipe.getUsbEndpoint();
final byte direction = endpoint.getDirection();
final byte type = endpoint.getType();
if (type == UsbConst.ENDPOINT_TYPE_CONTROL)
{
processControlIrp((UsbControlIrp) irp);
return;
}
switch (direction)
{
case UsbConst.ENDPOINT_DIRECTION_OUT: // 发送
irp.setActualLength(write(irp.getData(), irp.getOffset(),
irp.getLength()));
if (irp.getActualLength() < irp.getLength()
&& !irp.getAcceptShortPacket())
{
throw new UsbShortPacketException();
}
break;
case UsbConst.ENDPOINT_DIRECTION_IN: // 接收
irp.setActualLength(read(irp.getData(), irp.getOffset(),
irp.getLength()));
if (irp.getActualLength() < irp.getLength()
&& !irp.getAcceptShortPacket())
{
throw new UsbShortPacketException();
}
break;
default:
throw new UsbException("Invalid direction: "
+ direction);
}
}
我们好好琢磨下switch中的两个case,一个对应发送,一个对应接收。这两个分支分别调用了write和read方法。
private int read(final byte[] data, final int offset, final int len) throws UsbException
{
final UsbEndpointDescriptor descriptor = getEndpointDescriptor();
final byte type = this.pipe.getUsbEndpoint().getType();
final DeviceHandle handle = getDevice().open();
int read = 0;
while (read < len)
{
final int size =
Math.min(len - read, descriptor.wMaxPacketSize() & 0xffff);
final ByteBuffer buffer = ByteBuffer.allocateDirect(size); // 采用nio的方法
final int result = transfer(handle, descriptor, type, buffer);
buffer.rewind();
buffer.get(data, offset + read, result);
read += result;
// Short packet detected, aborting
if (result < size) break;
}
return read;
}
这里说明一下short packet:
在使用irp时,可以设置irp是否使用short packet模式,如果设置了,则显然根据上述代码可以推断,irp会在未装满数据的情况下返回,并且触发finish事件,然后进入下一个irp的处理。如果没有设置short packet呢?在没装满数据时while循环条件会一直不满足,所以线程应该会阻塞。
read代码是这样的,那write代码short packet是怎么工作的呢?
private int write(final byte[] data, final int offset, final int len) throws UsbException
{
final UsbEndpointDescriptor descriptor = getEndpointDescriptor();
final byte type = this.pipe.getUsbEndpoint().getType();
final DeviceHandle handle = getDevice().open();
int written = 0;
while (written < len)
{
final int size =
Math.min(len - written, descriptor.wMaxPacketSize() & 0xffff);
final ByteBuffer buffer = ByteBuffer.allocateDirect(size);
buffer.put(data, offset + written, size);
buffer.rewind();
final int result = transfer(handle, descriptor, type, buffer);
written += result;
// Short packet detected, aborting
if (result < size) break;
}
return written;
}
同样的,在write操作中,没开启short packet的话,如果需要发送的信息小于packet最大尺寸,信息会发出去,但是线程在发送完信息后会阻塞,这样就不会触发finish事件。
这样会有什么问题呢?想象一下线程一直卡在一个irp包上,后续submit的包就不会受理,也就是后续信息发不出去,这并不是我们想要的。所以应该始终开启short packet模式。
开启short packet后需要注意的是,在处理接收事件时,一定要根据irp的actualLength来读data,而不是length,因为该包很可能没装满。
usb4java默认始终开启short packet模式。