
net.codecrete.usb.macos.MacosUsbDevice Maven / Gradle / Ivy
//
// Java Does USB
// Copyright (c) 2022 Manuel Bleichenbacher
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//
package net.codecrete.usb.macos;
import net.codecrete.usb.UsbControlTransfer;
import net.codecrete.usb.UsbDevice;
import net.codecrete.usb.UsbDirection;
import net.codecrete.usb.UsbRecipient;
import net.codecrete.usb.UsbRequestType;
import net.codecrete.usb.UsbTransferType;
import net.codecrete.usb.common.ScopeCleanup;
import net.codecrete.usb.common.Transfer;
import net.codecrete.usb.common.UsbDeviceImpl;
import net.codecrete.usb.macos.gen.iokit.IOKit;
import net.codecrete.usb.macos.gen.iokit.IOUSBDevRequest;
import net.codecrete.usb.macos.gen.iokit.IOUSBFindInterfaceRequest;
import net.codecrete.usb.usbstandard.ConfigurationDescriptor;
import net.codecrete.usb.usbstandard.Constants;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
import static java.lang.foreign.ValueLayout.JAVA_INT;
import static java.lang.foreign.ValueLayout.JAVA_SHORT;
import static net.codecrete.usb.common.ForeignMemory.dereference;
import static net.codecrete.usb.macos.MacosUsbException.throwException;
/**
* MacOS implementation of {@link UsbDevice}.
*
* All read and write operations on endpoints are submitted through synchronized methods in order to control
* concurrency. If it wasn't controlled, the danger is that device and interface pointers are used, which have
* just been closed and thus deallocated by another thread, likely leading to crashes.
*
*
* As a consequence of the synchronized submission, blocking operations consists of submitting an
* asynchronous transfer and waiting for the completion.
*
*/
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "java:S2160"})
public class MacosUsbDevice extends UsbDeviceImpl {
private final MacosAsyncTask asyncTask;
// Native USB device interface (IOUSBDeviceInterface**)
private MemorySegment device;
// Currently selected configuration
private int configurationValue;
// Details about interfaces that have been claimed
private List claimedInterfaces;
// Details about endpoints of current alternate settings (for claimed interfaces)
private Map endpoints;
private final long discoveryTime;
MacosUsbDevice(MemorySegment device, Object id, int vendorId, int productId) {
super(id, vendorId, productId);
discoveryTime = System.currentTimeMillis();
asyncTask = MacosAsyncTask.INSTANCE;
loadDescription(device);
this.device = device;
IoKitUsb.AddRef(device);
}
@Override
public synchronized void detachStandardDrivers() {
checkIsClosed("detachStandardDrivers() must not be called while the device is open");
var ret = IoKitUsb.USBDeviceReEnumerate(device, IOKit.kUSBReEnumerateCaptureDeviceMask());
if (ret != 0)
throwException(ret, "detaching standard drivers failed");
}
@Override
public synchronized void attachStandardDrivers() {
checkIsClosed("attachStandardDrivers() must not be called while the device is open");
var ret = IoKitUsb.USBDeviceReEnumerate(device, IOKit.kUSBReEnumerateReleaseDeviceMask());
if (ret != 0)
throwException(ret, "attaching standard drivers failed");
}
@Override
public boolean isOpened() {
return claimedInterfaces != null;
}
@SuppressWarnings("java:S2276")
@Override
public synchronized void open() {
checkIsClosed("device is already open");
// open device (several retries if device has just been connected/discovered)
var duration = System.currentTimeMillis() - discoveryTime;
var numTries = duration < 1000 ? 4 : 1;
var ret = 0;
while (numTries > 0) {
numTries -= 1;
ret = IoKitUsb.USBDeviceOpenSeize(device);
if (ret != IOKit.kIOReturnExclusiveAccess())
break;
// sleep and retry
try {
Thread.sleep(90);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (ret != 0)
throwException(ret, "opening USB device failed");
claimedInterfaces = new ArrayList<>();
addDeviceEventSource();
// set configuration
ret = IoKitUsb.SetConfiguration(device, (byte) configurationValue);
if (ret != 0)
throwException(ret, "setting configuration failed");
updateEndpointList();
}
@Override
public synchronized void close() {
if (!isOpened())
return;
for (var interfaceInfo : claimedInterfaces) {
setClaimed(interfaceInfo.interfaceNumber, false);
var source = IoKitUsb.GetInterfaceAsyncEventSource(interfaceInfo.iokitInterface());
IoKitUsb.USBInterfaceClose(interfaceInfo.iokitInterface);
IoKitUsb.Release(interfaceInfo.iokitInterface);
if (source.address() != 0)
asyncTask.removeEventSource(source);
}
claimedInterfaces = null;
endpoints = null;
var source = IoKitUsb.GetDeviceAsyncEventSource(device);
IoKitUsb.USBDeviceClose(device);
if (source.address() != 0)
asyncTask.removeEventSource(source);
}
@Override
protected synchronized void disconnect() {
super.disconnect();
IoKitUsb.Release(device);
device = null;
}
private void loadDescription(MemorySegment device) {
try (var arena = Arena.ofConfined()) {
// retrieve device descriptor using synchronous control transfer
var data = arena.allocate(255);
var deviceRequest = createDeviceRequest(arena, UsbDirection.IN, new UsbControlTransfer(
UsbRequestType.STANDARD,
UsbRecipient.DEVICE,
6, // get descriptor
Constants.DEVICE_DESCRIPTOR_TYPE << 8,
0
), data);
var ret = IoKitUsb.DeviceRequest(device, deviceRequest);
if (ret != 0)
throwException(ret, "querying device descriptor failed");
var len = IOUSBDevRequest.wLenDone(deviceRequest);
rawDeviceDescriptor = data.asSlice(0, len).toArray(JAVA_BYTE);
configurationValue = 0;
// retrieve information of first configuration
var descPtrHolder = arena.allocate(ADDRESS);
ret = IoKitUsb.GetConfigurationDescriptorPtr(device, (byte) 0, descPtrHolder);
if (ret != 0)
throwException(ret, "querying first configuration failed");
var configDesc = dereference(descPtrHolder).reinterpret(999999);
var configDescHeader = new ConfigurationDescriptor(configDesc);
configDesc = configDesc.asSlice(0, configDescHeader.totalLength());
var configuration = setConfigurationDescriptor(configDesc);
configurationValue = 255 & configuration.configValue();
}
}
@SuppressWarnings("java:S135")
private InterfaceInfo findInterfaceInfo(int interfaceNumber) {
try (var arena = Arena.ofConfined(); var outerCleanup = new ScopeCleanup()) {
var request = IOUSBFindInterfaceRequest.allocate(arena);
IOUSBFindInterfaceRequest.bInterfaceClass(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
IOUSBFindInterfaceRequest.bInterfaceSubClass(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
IOUSBFindInterfaceRequest.bInterfaceProtocol(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
IOUSBFindInterfaceRequest.bAlternateSetting(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
var iterHolder = arena.allocate(JAVA_INT);
var ret = IoKitUsb.CreateInterfaceIterator(device, request, iterHolder);
if (ret != 0)
throwException("internal error (CreateInterfaceIterator)");
final var iter = iterHolder.get(JAVA_INT, 0);
outerCleanup.add(() -> IOKit.IOObjectRelease(iter));
var intfNumberHolder = arena.allocate(JAVA_INT);
int service;
while ((service = IOKit.IOIteratorNext(iter)) != 0) {
try (var cleanup = new ScopeCleanup()) {
final var service_final = service;
cleanup.add(() -> IOKit.IOObjectRelease(service_final));
final var intf = IoKitHelper.getInterface(service, IoKitHelper.kIOUSBInterfaceUserClientTypeID,
IoKitHelper.kIOUSBInterfaceInterfaceID190);
if (intf == null)
continue;
cleanup.add(() -> IoKitUsb.Release(intf));
IoKitUsb.GetInterfaceNumber(intf, intfNumberHolder);
if (intfNumberHolder.get(JAVA_INT, 0) != interfaceNumber)
continue;
IoKitUsb.AddRef(intf);
return new InterfaceInfo(intf, interfaceNumber);
}
}
}
throwException("invalid interface number: %d", interfaceNumber);
throw new AssertionError("not reached");
}
public synchronized void claimInterface(int interfaceNumber) {
checkIsOpen();
getInterfaceWithCheck(interfaceNumber, false);
try (var cleanup = new ScopeCleanup()) {
var interfaceInfo = findInterfaceInfo(interfaceNumber);
cleanup.add(() -> IoKitUsb.Release(interfaceInfo.iokitInterface()));
var ret = IoKitUsb.USBInterfaceOpen(interfaceInfo.iokitInterface());
if (ret != 0)
throwException(ret, "claiming interface failed");
IoKitUsb.AddRef(interfaceInfo.iokitInterface());
claimedInterfaces.add(interfaceInfo);
setClaimed(interfaceNumber, true);
addInterfaceEventSource(interfaceInfo);
}
updateEndpointList();
}
@SuppressWarnings({"OptionalGetWithoutIsPresent", "java:S3655"})
public synchronized void selectAlternateSetting(int interfaceNumber, int alternateNumber) {
// check interface
var intf = getInterfaceWithCheck(interfaceNumber, true);
// check alternate setting
var altSetting = intf.getAlternate(alternateNumber);
var intfInfo =
claimedInterfaces.stream().filter(interf -> interf.interfaceNumber() == interfaceNumber).findFirst().get();
var ret = IoKitUsb.SetAlternateInterface(intfInfo.iokitInterface(), (byte) alternateNumber);
if (ret != 0)
throwException(ret, "setting alternate interface failed");
intf.setAlternate(altSetting);
updateEndpointList();
}
@SuppressWarnings({"OptionalGetWithoutIsPresent", "java:S3655"})
public synchronized void releaseInterface(int interfaceNumber) {
checkIsOpen();
getInterfaceWithCheck(interfaceNumber, true);
@SuppressWarnings("OptionalGetWithoutIsPresent")
var interfaceInfo =
claimedInterfaces.stream().filter(info -> info.interfaceNumber == interfaceNumber).findFirst().get();
var source = IoKitUsb.GetInterfaceAsyncEventSource(interfaceInfo.iokitInterface());
if (source.address() != 0)
asyncTask.removeEventSource(source);
var ret = IoKitUsb.USBInterfaceClose(interfaceInfo.iokitInterface());
if (ret != 0)
throwException(ret, "releasing interface failed");
claimedInterfaces.remove(interfaceInfo);
IoKitUsb.Release(interfaceInfo.iokitInterface());
setClaimed(interfaceNumber, false);
updateEndpointList();
}
/**
* Update the map of active endpoints.
*
* MacOS uses a pipe index to refer to endpoints. This method
* builds a map from endpoint address to pipe index.
*
*/
private void updateEndpointList() {
endpoints = new HashMap<>();
try (var arena = Arena.ofConfined()) {
var directionHolder = arena.allocate(JAVA_BYTE);
var numberHolder = arena.allocate(JAVA_BYTE);
var transferTypeHolder = arena.allocate(JAVA_BYTE);
var maxPacketSizeHolder = arena.allocate(JAVA_SHORT);
var intervalHolder = arena.allocate(JAVA_BYTE);
for (var interfaceInfo : claimedInterfaces) {
var intf = interfaceInfo.iokitInterface();
var numEndpointsHolder = arena.allocate(JAVA_BYTE);
var ret = IoKitUsb.GetNumEndpoints(intf, numEndpointsHolder);
if (ret != 0)
throwException(ret, "internal error (GetNumEndpoints)");
var numEndpoints = numEndpointsHolder.get(JAVA_BYTE, 0) & 255;
for (var pipeIndex = 1; pipeIndex <= numEndpoints; pipeIndex++) {
ret = IoKitUsb.GetPipeProperties(intf, (byte) pipeIndex, directionHolder, numberHolder,
transferTypeHolder, maxPacketSizeHolder, intervalHolder);
if (ret != 0)
throwException(ret, "internal error (GetPipeProperties)");
var endpointNumber = numberHolder.get(JAVA_BYTE, 0) & 0xff;
var direction = directionHolder.get(JAVA_BYTE, 0) & 0xff;
var endpointAddress = (byte) (endpointNumber | (direction << 7));
var transferType = transferTypeHolder.get(JAVA_BYTE, 0);
var maxPacketSize = maxPacketSizeHolder.get(JAVA_SHORT, 0) & 0xffff;
var endpointInfo = new EndpointInfo(interfaceInfo.iokitInterface(), (byte) pipeIndex,
getTransferType(transferType), maxPacketSize);
endpoints.put(endpointAddress, endpointInfo);
}
}
}
}
@SuppressWarnings("SameParameterValue")
private synchronized EndpointInfo getEndpointInfo(int endpointNumber, UsbDirection direction,
UsbTransferType transferType1, UsbTransferType transferType2) {
if (endpoints != null) {
var endpointAddress = (byte) (endpointNumber | (direction == UsbDirection.IN ? 0x80 : 0));
var endpointInfo = endpoints.get(endpointAddress);
if (endpointInfo != null && (endpointInfo.transferType == transferType1 || endpointInfo.transferType == transferType2))
return endpointInfo;
}
String transferTypeDesc;
if (transferType2 == null)
transferTypeDesc = transferType1.name();
else
transferTypeDesc = String.format("%s or %s", transferType1.name(), transferType2.name());
throwException(
"endpoint number %d does not exist, is not part of a claimed interface or is not valid for %s transfer in %s direction",
endpointNumber, transferTypeDesc, direction.name());
throw new AssertionError("not reached");
}
private static MemorySegment createDeviceRequest(Arena arena, UsbDirection direction, UsbControlTransfer setup,
MemorySegment data) {
var deviceRequest = IOUSBDevRequest.allocate(arena);
var bmRequestType =
(direction == UsbDirection.IN ? 0x80 : 0x00) | (setup.requestType().ordinal() << 5) | setup.recipient().ordinal();
IOUSBDevRequest.bmRequestType(deviceRequest, (byte) bmRequestType);
IOUSBDevRequest.bRequest(deviceRequest, (byte) setup.request());
IOUSBDevRequest.wValue(deviceRequest, (short) setup.value());
IOUSBDevRequest.wIndex(deviceRequest, (short) setup.index());
IOUSBDevRequest.wLength(deviceRequest, (short) data.byteSize());
IOUSBDevRequest.pData(deviceRequest, data);
return deviceRequest;
}
@Override
public byte @NotNull [] controlTransferIn(@NotNull UsbControlTransfer setup, int length) {
try (var arena = Arena.ofConfined()) {
var data = arena.allocate(length);
var deviceRequest = createDeviceRequest(arena, UsbDirection.IN, setup, data);
var transfer = new MacosTransfer();
transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
synchronized (transfer) {
submitControlTransfer(deviceRequest, transfer);
waitForTransfer(transfer, 0, UsbDirection.IN, 0);
}
return data.asSlice(0, transfer.resultSize()).toArray(JAVA_BYTE);
}
}
@Override
public void controlTransferOut(@NotNull UsbControlTransfer setup, byte[] data) {
try (var arena = Arena.ofConfined()) {
var dataLength = data != null ? data.length : 0;
var dataSegment = arena.allocate(dataLength);
if (dataLength > 0)
dataSegment.copyFrom(MemorySegment.ofArray(data));
var deviceRequest = createDeviceRequest(arena, UsbDirection.OUT, setup, dataSegment);
var transfer = new MacosTransfer();
transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
synchronized (transfer) {
submitControlTransfer(deviceRequest, transfer);
waitForTransfer(transfer, 0, UsbDirection.OUT, 0);
}
}
}
@Override
public void transferOut(int endpointNumber, byte @NotNull [] data, int offset, int length, int timeout) {
var epInfo = getEndpointInfo(endpointNumber, UsbDirection.OUT, UsbTransferType.BULK,
UsbTransferType.INTERRUPT);
try (var arena = Arena.ofConfined()) {
var nativeData = arena.allocate(JAVA_BYTE, length);
nativeData.copyFrom(MemorySegment.ofArray(data).asSlice(offset, length));
var transfer = new MacosTransfer();
transfer.setData(nativeData);
transfer.setDataSize(length);
transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
synchronized (transfer) {
if (timeout <= 0 || epInfo.transferType() == UsbTransferType.BULK) {
// no timeout or timeout handled by operating system
submitTransferOut(endpointNumber, transfer, timeout);
waitForTransfer(transfer, 0, UsbDirection.OUT, endpointNumber);
} else {
// interrupt transfer with timeout
submitTransferOut(endpointNumber, transfer, 0);
waitForTransfer(transfer, timeout, UsbDirection.OUT, endpointNumber);
}
}
}
}
@Override
public byte @NotNull [] transferIn(int endpointNumber, int timeout) {
var epInfo = getEndpointInfo(endpointNumber, UsbDirection.IN, UsbTransferType.BULK,
UsbTransferType.INTERRUPT);
try (var arena = Arena.ofConfined()) {
var nativeData = arena.allocate(JAVA_BYTE, epInfo.packetSize());
var transfer = new MacosTransfer();
transfer.setData(nativeData);
transfer.setDataSize(epInfo.packetSize());
transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
synchronized (transfer) {
if (timeout <= 0 || epInfo.transferType() == UsbTransferType.BULK) {
// no timeout, or timeout handled by operating system
submitTransferIn(endpointNumber, transfer, timeout);
waitForTransfer(transfer, 0, UsbDirection.IN, endpointNumber);
} else {
// interrupt transfer with timeout
submitTransferIn(endpointNumber, transfer, 0);
waitForTransfer(transfer, timeout, UsbDirection.IN, endpointNumber);
}
}
return nativeData.asSlice(0, transfer.resultSize()).toArray(JAVA_BYTE);
}
}
/**
* Submits a transfer IN to the specified BULK or INTERRUPT endpoint.
*
* A timeout may only be specified for BULK endpoints.
*
*
* @param endpointNumber endpoint number
* @param transfer transfer to execute
* @param timeout the timeout, in milliseconds, or 0 for no timeout
*/
synchronized void submitTransferIn(int endpointNumber, MacosTransfer transfer, int timeout) {
var epInfo = getEndpointInfo(endpointNumber, UsbDirection.IN, UsbTransferType.BULK,
UsbTransferType.INTERRUPT);
asyncTask.prepareForSubmission(transfer);
// submit transfer
int ret;
if (timeout <= 0)
ret = IoKitUsb.ReadPipeAsync(epInfo.iokitInterface(), epInfo.pipeIndex(), transfer.data(),
transfer.dataSize(), asyncTask.nativeCompletionCallback(), MemorySegment.ofAddress(transfer.id()));
else
ret = IoKitUsb.ReadPipeAsyncTO(epInfo.iokitInterface(), epInfo.pipeIndex(), transfer.data(),
transfer.dataSize(), timeout, timeout, asyncTask.nativeCompletionCallback(),
MemorySegment.ofAddress(transfer.id()));
if (ret != 0)
throwException(ret, "error occurred while reading from endpoint %d", endpointNumber);
}
/**
* Submits a transfer OUT to the specified BULK or INTERRUPT endpoint.
*
* A timeout may only be specified for BULK endpoints.
*
*
* @param endpointNumber endpoint number
* @param transfer transfer request to execute
* @param timeout the timeout, in milliseconds, or 0 for no timeout
*/
synchronized void submitTransferOut(int endpointNumber, MacosTransfer transfer, int timeout) {
var epInfo = getEndpointInfo(endpointNumber, UsbDirection.OUT, UsbTransferType.BULK,
UsbTransferType.INTERRUPT);
asyncTask.prepareForSubmission(transfer);
// submit transfer
int ret;
if (timeout <= 0)
ret = IoKitUsb.WritePipeAsync(epInfo.iokitInterface(), epInfo.pipeIndex(), transfer.data(),
transfer.dataSize(), asyncTask.nativeCompletionCallback(), MemorySegment.ofAddress(transfer.id()));
else
ret = IoKitUsb.WritePipeAsyncTO(epInfo.iokitInterface(), epInfo.pipeIndex(), transfer.data(),
transfer.dataSize(), timeout, timeout, asyncTask.nativeCompletionCallback(),
MemorySegment.ofAddress(transfer.id()));
if (ret != 0)
throwException(ret, "error occurred while transmitting to endpoint %d", endpointNumber);
}
/**
* Submits a control transfer.
*
* @param deviceRequest control transfer request
* @param transfer transfer request (for completion handling)
*/
synchronized void submitControlTransfer(MemorySegment deviceRequest, MacosTransfer transfer) {
checkIsOpen();
asyncTask.prepareForSubmission(transfer);
// submit transfer
var ret = IoKitUsb.DeviceRequestAsync(device, deviceRequest, asyncTask.nativeCompletionCallback(),
MemorySegment.ofAddress(transfer.id()));
if (ret != 0)
throwException(ret, "control transfer failed");
}
@Override
protected Transfer createTransfer() {
return new MacosTransfer();
}
@Override
public synchronized void abortTransfers(UsbDirection direction, int endpointNumber) {
var epInfo = getEndpointInfo(endpointNumber, direction, UsbTransferType.BULK,
UsbTransferType.INTERRUPT);
var ret = IoKitUsb.AbortPipe(epInfo.iokitInterface(), epInfo.pipeIndex());
if (ret != 0)
throwException(ret, "aborting transfers failed");
}
@Override
public synchronized void clearHalt(UsbDirection direction, int endpointNumber) {
var epInfo = getEndpointInfo(endpointNumber, direction, UsbTransferType.BULK,
UsbTransferType.INTERRUPT);
var ret = IoKitUsb.ClearPipeStallBothEnds(epInfo.iokitInterface(), epInfo.pipeIndex());
if (ret != 0)
throwException(ret, "clearing halt condition failed");
}
@Override
public synchronized @NotNull InputStream openInputStream(int endpointNumber, int bufferSize) {
// check that endpoint number is valid
getEndpointInfo(endpointNumber, UsbDirection.IN, UsbTransferType.BULK, null);
return new MacosEndpointInputStream(this, endpointNumber, bufferSize);
}
@Override
public synchronized @NotNull OutputStream openOutputStream(int endpointNumber, int bufferSize) {
// check that endpoint number is valid
getEndpointInfo(endpointNumber, UsbDirection.OUT, UsbTransferType.BULK, null);
return new MacosEndpointOutputStream(this, endpointNumber, bufferSize);
}
@Override
protected void throwOSException(int errorCode, String message, Object... args) {
throwException(errorCode, message, args);
}
private static UsbTransferType getTransferType(byte macosTransferType) {
return switch (macosTransferType) {
case 1 -> UsbTransferType.ISOCHRONOUS;
case 2 -> UsbTransferType.BULK;
case 3 -> UsbTransferType.INTERRUPT;
default -> null;
};
}
private synchronized void addDeviceEventSource() {
try (var innerArena = Arena.ofConfined()) {
var sourceHolder = innerArena.allocate(ADDRESS);
var ret = IoKitUsb.CreateDeviceAsyncEventSource(device, sourceHolder);
if (ret != 0)
throwException(ret, "internal error (CreateDeviceAsyncEventSource)");
var source = dereference(sourceHolder);
asyncTask.addEventSource(source);
}
}
private synchronized void addInterfaceEventSource(InterfaceInfo interfaceInfo) {
try (var innerArena = Arena.ofConfined()) {
var sourceHolder = innerArena.allocate(ADDRESS);
var ret = IoKitUsb.CreateInterfaceAsyncEventSource(interfaceInfo.iokitInterface(), sourceHolder);
if (ret != 0)
throwException(ret, "internal error (CreateInterfaceAsyncEventSource)");
var source = dereference(sourceHolder);
asyncTask.addEventSource(source);
}
}
record InterfaceInfo(MemorySegment iokitInterface, int interfaceNumber) {
}
record EndpointInfo(MemorySegment iokitInterface, byte pipeIndex, UsbTransferType transferType, int packetSize) {
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy