安卓UVC控制協(xié)議入門

最近的項(xiàng)目里面需要對(duì)UVC攝像頭進(jìn)行操控,簡(jiǎn)單的了解了下相關(guān)的知識(shí)。

首先UVC全稱為USB video(device) class,是微軟與另外幾家設(shè)備廠商聯(lián)合推出的為USB視頻捕獲設(shè)備定義的協(xié)議標(biāo)準(zhǔn),目前已成為USB org標(biāo)準(zhǔn)之一。在USB中文網(wǎng)上其實(shí)有比較詳細(xì)的描述,但是新手直接上來(lái)就看這個(gè)協(xié)議其實(shí)是比較懵逼的。所以可以參考UVCCamera這個(gè)安卓項(xiàng)目的源碼去輔助理解。

USB描述符

當(dāng)我們連接到一個(gè)UVC設(shè)備之后其實(shí)第一步應(yīng)該先獲取它的描述符來(lái)看看它具體支持哪些操作。UVCCamera里是使用libusb獲取到USB的設(shè)備描述符之后在uvc_scan_control里面解析的。實(shí)際上我們也可以利用安卓應(yīng)用層的UsbDeviceConnection.getRawDescriptors接口獲取到USB的描述符然后再java層解析:

val manager = context.getSystemService(AppCompatActivity.USB_SERVICE) as UsbManager
val deviceIterator: Iterator<UsbDevice> = manager.deviceList.values.iterator()
while (deviceIterator.hasNext()) {
    val device = deviceIterator.next()
    if (device.vendorId == targetVid && device.productId == targetPid) {
        val connect = manager.openDevice(device)
        val desc = connect.rawDescriptors
        // 解析usb描述符
        connect.close()
        return
    }
}

這里拿到的是一個(gè)byte數(shù)組,我們先要理解什么是usb描述符才能去解析它。usb描述符其實(shí)就是描述usb的屬性和用途的,四種主要描述符的邏輯結(jié)構(gòu)大概如下:

1.png
  • 設(shè)備描述符: 每一個(gè)USB設(shè)備只有一個(gè)設(shè)備描述符,主要向主機(jī)說(shuō)明設(shè)備類型、端點(diǎn)0最大包長(zhǎng)、設(shè)備版本、配置數(shù)量等等
  • 配置描述符: 每一個(gè)USB設(shè)備至少有一個(gè)或者多個(gè)配置描述符,但是主機(jī)同一時(shí)間只能選擇某一種配置,標(biāo)準(zhǔn)配置描述符主要向主機(jī)描述當(dāng)前配置下的設(shè)備屬性、所需電流、支持的接口數(shù)、配置描述符集合長(zhǎng)度等等。
  • 接口描述符 : 每一個(gè)USB配置下至少有一個(gè)或者多個(gè)接口描述符,接口描述符主要說(shuō)明設(shè)備類型、此接口下使用的端點(diǎn)數(shù)(不包括0號(hào)端點(diǎn)),一個(gè)接口就是實(shí)現(xiàn)一種功能,實(shí)現(xiàn)這種功能可能需要端點(diǎn)0就夠了,可能還需要其它的端點(diǎn)配合。
  • 端點(diǎn)描述符: 每一個(gè)USB接口下可以有多個(gè)端點(diǎn)描述符,端點(diǎn)描述符用來(lái)描述符端點(diǎn)的各種屬性。

端點(diǎn)是實(shí)現(xiàn)USB設(shè)備功能的物理緩沖區(qū)實(shí)體,USB主機(jī)和設(shè)備是通過(guò)端點(diǎn)進(jìn)行數(shù)據(jù)交互的

描述符解析

所有類型的描述符的前兩個(gè)字節(jié)定義都是一樣的,第一個(gè)字節(jié)指定描述符的長(zhǎng)度,而第二個(gè)字節(jié)表示描述符類型。所以我們拿到rawDescriptors之后可以用下面的代碼去遍歷解析:

var index = 0
val descriptorTypes = mapOf(
    0x01.toByte() to "DEVICE",
    0x02.toByte() to "CONFIG",
    0x04.toByte() to "INTERFACE",
    0x05.toByte() to "ENDPOINT",
)

while (index < desc.size) {
    descriptorTypes[desc[index + 1]]?.let {
        val indent = " ".repeat(desc[index + 1].toInt())
        Log.d(TAG, "${indent}$it")
    }
    index += desc[index]
}

我這個(gè)調(diào)試設(shè)備的描述符解析如下:

 DEVICE
  CONFIG
    INTERFACE
     ENDPOINT
    INTERFACE
     ENDPOINT
    INTERFACE
     ENDPOINT
     ENDPOINT

可以看到它有一個(gè)設(shè)備描述符,這個(gè)設(shè)備描述符下有個(gè)一個(gè)配置描述符,這個(gè)配置描述符下有三個(gè)接口描述符,每個(gè)接口描述符下又有一到兩個(gè)端點(diǎn)描述符。

知道了描述符的類型就能在USB標(biāo)準(zhǔn)里找到它具體的數(shù)據(jù)結(jié)構(gòu)去解析,例如設(shè)備描述符的定義如下:

struct libusb_device_descriptor {
    /** Size of this descriptor (in bytes) */
    uint8_t  bLength;

    /** Descriptor type. Will have value
     * \ref libusb_descriptor_type::LIBUSB_DT_DEVICE LIBUSB_DT_DEVICE in this
     * context. */
    uint8_t  bDescriptorType;

    /** USB specification release number in binary-coded decimal. A value of
     * 0x0200 indicates USB 2.0, 0x0110 indicates USB 1.1, etc. */
    uint16_t bcdUSB;

    /** USB-IF class code for the device. See \ref libusb_class_code. */
    uint8_t  bDeviceClass;

    /** USB-IF subclass code for the device, qualified by the bDeviceClass
     * value */
    uint8_t  bDeviceSubClass;

    /** USB-IF protocol code for the device, qualified by the bDeviceClass and
     * bDeviceSubClass values */
    uint8_t  bDeviceProtocol;

    /** Maximum packet size for endpoint 0 */
    uint8_t  bMaxPacketSize0;

    /** USB-IF vendor ID */
    uint16_t idVendor;

    /** USB-IF product ID */
    uint16_t idProduct;

    /** Device release number in binary-coded decimal */
    uint16_t bcdDevice;

    /** Index of string descriptor describing manufacturer */
    uint8_t  iManufacturer;

    /** Index of string descriptor describing product */
    uint8_t  iProduct;

    /** Index of string descriptor containing device serial number */
    uint8_t  iSerialNumber;

    /** Number of possible configurations */
    uint8_t  bNumConfigurations;
};

描述符類型定義的id可以參考lsusb的libusb_descriptor_type枚舉:

enum libusb_descriptor_type {
    /** Device descriptor. See libusb_device_descriptor. */
    LIBUSB_DT_DEVICE = 0x01,

    /** Configuration descriptor. See libusb_config_descriptor. */
    LIBUSB_DT_CONFIG = 0x02,

    /** String descriptor */
    LIBUSB_DT_STRING = 0x03,

    /** Interface descriptor. See libusb_interface_descriptor. */
    LIBUSB_DT_INTERFACE = 0x04,

    /** Endpoint descriptor. See libusb_endpoint_descriptor. */
    LIBUSB_DT_ENDPOINT = 0x05,

    /** Interface Association Descriptor.
    * See libusb_interface_association_descriptor */
    LIBUSB_DT_INTERFACE_ASSOCIATION = 0x0b,

    /** BOS descriptor */
    LIBUSB_DT_BOS = 0x0f,

    /** Device Capability descriptor */
    LIBUSB_DT_DEVICE_CAPABILITY = 0x10,

    /** HID descriptor */
    LIBUSB_DT_HID = 0x21,

    /** HID report descriptor */
    LIBUSB_DT_REPORT = 0x22,

    /** Physical descriptor */
    LIBUSB_DT_PHYSICAL = 0x23,

    /** Hub descriptor */
    LIBUSB_DT_HUB = 0x29,

    /** SuperSpeed Hub descriptor */
    LIBUSB_DT_SUPERSPEED_HUB = 0x2a,

    /** SuperSpeed Endpoint Companion descriptor */
    LIBUSB_DT_SS_ENDPOINT_COMPANION = 0x30
};

可以看到描述符的類型其實(shí)不止上面四種,還有很多其他的類型。例如我就能從UVC 相機(jī)終端描述符里面的bmControls字段解析出相機(jī)具體支持的操作:

mControls:使用位圖來(lái)表示支持的視頻流。

  • D0:掃描模式 //掃描模式(逐行掃描或隔行掃描)
  • D1:自動(dòng)曝光模式
  • D2:自動(dòng)曝光優(yōu)先級(jí)
  • D3:曝光時(shí)間(絕對(duì)值)
  • D4:曝光時(shí)間(相對(duì))
  • D5:焦點(diǎn)(絕對(duì))
  • D6:焦點(diǎn)(相對(duì))
  • ...

libusb里面這個(gè)UVC相機(jī)終端描述符會(huì)作為接口描述符的拓展信息保存:

static int parse_interface(libusb_context *ctx,
    struct libusb_interface *usb_interface, const uint8_t *buffer, int size)
{
    ...
    begin = buffer;

    /* Skip over any interface, class or vendor descriptors */
    while (size >= DESC_HEADER_LENGTH) {
        ...
        /* If we find another "proper" descriptor then we're done */
        if (header->bDescriptorType == LIBUSB_DT_INTERFACE ||
            header->bDescriptorType == LIBUSB_DT_ENDPOINT ||
            header->bDescriptorType == LIBUSB_DT_CONFIG ||
            header->bDescriptorType == LIBUSB_DT_DEVICE)
            break;

        buffer += header->bLength;
        parsed += header->bLength;
        size -= header->bLength;
    }

    /* Copy any unknown descriptors into a storage area for */
    /*  drivers to later parse */
    ptrdiff_t len = buffer - begin;
    if (len > 0) {
        void *extra = malloc((size_t)len);
        ...
        memcpy(extra, begin, (size_t)len);
        ifp->extra = extra;
        ifp->extra_length = (int)len;
    }
    ...
}

所以在uvc_scan_control里面就從接口描述符的extra信息里面去解析UVC的相關(guān)描述符:

uvc_error_t uvc_scan_control(uvc_device_t *dev, uvc_device_info_t *info) {
    ...
    for (interface_idx = 0; interface_idx < info->config->bNumInterfaces; ++interface_idx) {
        if_desc = &info->config->interface[interface_idx].altsetting[0];
        MARK("interface_idx=%d:bInterfaceClass=%02x,bInterfaceSubClass=%02x", interface_idx, if_desc->bInterfaceClass, if_desc->bInterfaceSubClass);
        // select first found Video control
        if (if_desc->bInterfaceClass == LIBUSB_CLASS_VIDEO/*14*/ && if_desc->bInterfaceSubClass == 1) // Video, Control
            break;
        ...
    }
    ...
    buffer = if_desc->extra;
    buffer_left = if_desc->extra_length;

    while (buffer_left >= 3) { // parseX needs to see buf[0,2] = length,type
        block_size = buffer[0];
        parse_ret = uvc_parse_vc(dev, info, buffer, block_size);

        if (parse_ret != UVC_SUCCESS) {
            ret = parse_ret;
            break;
        }

        buffer_left -= block_size;
        buffer += block_size;
    }

    ...
}

uvc_error_t uvc_parse_vc(uvc_device_t *dev, uvc_device_info_t *info,
        const unsigned char *block, size_t block_size) {
    int descriptor_subtype;
    uvc_error_t ret = UVC_SUCCESS;

    UVC_ENTER();

    if (block[1] != LIBUSB_DT_CS_INTERFACE/*36*/) { // not a CS_INTERFACE descriptor??
        UVC_EXIT(UVC_SUCCESS);
        return UVC_SUCCESS; // UVC_ERROR_INVALID_DEVICE;
    }

    descriptor_subtype = block[2];

    switch (descriptor_subtype) {
    case UVC_VC_HEADER:
        ret = uvc_parse_vc_header(dev, info, block, block_size);
        break;
    case UVC_VC_INPUT_TERMINAL:
        ret = uvc_parse_vc_input_terminal(dev, info, block, block_size);
        break;
    case UVC_VC_OUTPUT_TERMINAL:
        break;
    case UVC_VC_SELECTOR_UNIT:
        break;
    case UVC_VC_PROCESSING_UNIT:
        ret = uvc_parse_vc_processing_unit(dev, info, block, block_size);
        break;
    case UVC_VC_EXTENSION_UNIT:
        ret = uvc_parse_vc_extension_unit(dev, info, block, block_size);
        break;
    default:
        LOGW("UVC_ERROR_INVALID_DEVICE:descriptor_subtype=0x%02x", descriptor_subtype);
        ret = UVC_ERROR_INVALID_DEVICE;
    }

    UVC_EXIT(ret);
    return ret;
}

只要找到bInterfaceClass等于14,bInterfaceSubClass等于1的視頻控制接口,然后在它的拓展信息里面找到UVC_VC_INPUT_TERMINAL(0x02)類型的描述符就是我們需要的UVC 相機(jī)終端描述符

USB通訊

前面有說(shuō)到端點(diǎn)是實(shí)現(xiàn)USB設(shè)備功能的物理緩沖區(qū)實(shí)體,USB主機(jī)和設(shè)備是通過(guò)端點(diǎn)進(jìn)行數(shù)據(jù)交互的,之前做HID設(shè)備通訊的時(shí)候流程是找到bInterfaceClassUsbConstants.USB_CLASS_HID(0x03)類型的接口,在它下面找到輸入端點(diǎn)去寫入請(qǐng)求,然后找到輸出端點(diǎn)去讀取設(shè)備響應(yīng)。

但是UVC的攝像頭控制并不是用視頻控制接口去讀寫,而是直接使用USB設(shè)備不屬于任何接口的0號(hào)端口去進(jìn)行通訊。

例如uvc_get_pantilt_abs里面在傳輸數(shù)據(jù)的時(shí)候就沒(méi)有指定端口號(hào):

uvc_error_t uvc_get_pantilt_abs(uvc_device_handle_t *devh, int32_t *pan, int32_t *tilt,
    enum uvc_req_code req_code) {

    uint8_t data[8];
    uvc_error_t ret;

    ret = libusb_control_transfer(devh->usb_devh, REQ_TYPE_GET, req_code,
            UVC_CT_PANTILT_ABSOLUTE_CONTROL << 8,
            devh->info->ctrl_if.input_term_descs->request,
            data, sizeof(data), CTRL_TIMEOUT_MILLIS);

    if (LIKELY(ret == sizeof(data))) {
        *pan = DW_TO_INT(data);
        *tilt = DW_TO_INT(data + 4);
        return UVC_SUCCESS;
    } else {
        return ret;
    }
}

因?yàn)樵?a target="_blank">libusb_control_transfer里面調(diào)用libusb_fill_control_transfer去填充信息的時(shí)候就會(huì)把端口指定為0號(hào)端口

int API_EXPORTED libusb_control_transfer(libusb_device_handle *dev_handle,
    uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue, uint16_t wIndex,
    unsigned char *data, uint16_t wLength, unsigned int timeout)
{
    ...
    libusb_fill_control_transfer(transfer, dev_handle, buffer,
        sync_transfer_cb, &completed, timeout); // 填充transfer信息
    transfer->flags = LIBUSB_TRANSFER_FREE_BUFFER;
    r = libusb_submit_transfer(transfer); // 發(fā)送請(qǐng)求
    if (UNLIKELY(r < 0)) {
        libusb_free_transfer(transfer);
        return r;
    }

    sync_transfer_wait_for_completion(transfer); // 等待回復(fù)
    ...

}

static inline void libusb_fill_control_transfer(
    struct libusb_transfer *transfer, libusb_device_handle *dev_handle,
    unsigned char *buffer, libusb_transfer_cb_fn callback, void *user_data,
    unsigned int timeout)
{
    struct libusb_control_setup *setup = (struct libusb_control_setup *)(void *) buffer;
    transfer->dev_handle = dev_handle;
    transfer->endpoint = 0; // 指定0號(hào)端口
    ...
}

涉及到使用USB進(jìn)行通訊的4種方式:

  • 控制傳輸 - 設(shè)備接入主機(jī)時(shí),需要通過(guò)控制傳輸去獲取USB設(shè)備的描述符以及對(duì)設(shè)備進(jìn)行識(shí)別,在設(shè)備的枚舉過(guò)程中都是使用控制傳輸進(jìn)行數(shù)據(jù)交換。
  • 同步傳輸 - 也叫等時(shí)傳輸,用于要求數(shù)據(jù)連續(xù)、實(shí)時(shí)且數(shù)據(jù)量大的場(chǎng)合,其對(duì)傳輸延時(shí)十分敏感,類似用于USB攝像設(shè)備,USB語(yǔ)音設(shè)備等等。
  • 中斷傳輸 - 用于數(shù)據(jù)量小的數(shù)據(jù)不連續(xù)的但實(shí)時(shí)性高的場(chǎng)合的一種傳輸方式,主要應(yīng)用于人機(jī)交互設(shè)備(HID)中的USB鼠標(biāo)和USB鍵盤等。
  • 批量傳輸 - 用于數(shù)據(jù)量大但對(duì)時(shí)間要求又不高的場(chǎng)合的一種傳輸方式,類似用于USB打印機(jī)和USB掃描儀等等。

控制傳輸

控制傳輸是usb設(shè)備一定會(huì)支持的傳輸方式,因?yàn)槊枋龇褪峭ㄟ^(guò)這種方式獲取的.

在安卓應(yīng)用層我們可以通過(guò)調(diào)用UsbDeviceConnection.controlTransfer來(lái)實(shí)現(xiàn),參考UVCCamera里面uvc_get_pantilt_abs里面獲取PanTilt值的c代碼,在java層可以用下面代碼獲取

private const val CONTROL_REQ_TYPE_GET = 0xa1
private const val UVC_GET_CUR = 0x81

val connection = usbManager.openDevice(device)

// 先claim bInterfaceClass為CC_VIDEO(0x0E) bInterfaceSubClass為SC_VIDEOCONTROL(0x01)的攝像頭控制接口
val vcInterface = UsbUtils.getInterface(device, UsbConstants.USB_CLASS_VIDEO, USB_SUBCLASS_VIDEO_CONTROL)
connection.claimInterface(vcInterface, true)

// 然后發(fā)送控制指令獲取PanTilt絕對(duì)值
val buff = ByteArray(8)
val index = getPanTiltControlIndex(connection)
val value = CT_PANTILT_ABSOLUTE_CONTROL.shl(8)
connection.controlTransfer(CONTROL_REQ_TYPE_GET, UVC_GET_CUR, value, index, buff, buff.size, 100)

// buff前四個(gè)byte組合起來(lái)是pan值
// buff后四個(gè)byte組合起來(lái)是tilt值
val pan = bytes[0].toUByte().toInt().shl(0) or
bytes[1].toUByte().toInt().shl(8) or
bytes[2].toUByte().toInt().shl(16) or
bytes[3].toUByte().toInt().shl(24)

val tilt = bytes[4].toUByte().toInt().shl(0) or
bytes[5].toUByte().toInt().shl(8) or
bytes[6].toUByte().toInt().shl(16) or
bytes[7].toUByte().toInt().shl(24)

connection.releaseInterface(usbInterface)
connection.close()

這里解釋下上面的值如何來(lái)的,首先看GET_CUR的文檔:

requestType request value index buffer length
10100001(接口或?qū)嶓w)
— — — — —
10100010(端點(diǎn))
GET_CUR
GET_MIN
GET_MAX
GET_RES
GET_LEN
GET_INFO
GET_DEF
UVC中大多數(shù)情況下取值都為控制選擇器CS(高字節(jié)),低字節(jié)為零。當(dāng)實(shí)體ID取不同值時(shí)則該字段取值也會(huì)有所不同 實(shí)體ID(高字節(jié))、接口(低字節(jié))
— — — — —
端點(diǎn)(低字節(jié))
用來(lái)接收數(shù)據(jù)或者發(fā)送數(shù)據(jù)的buffer buffer的大小

value

例如我們現(xiàn)在要獲取PanTilt的絕對(duì)值,那么在value字段部分文檔里面可以看到當(dāng)Entity ID值為Camera Terminal時(shí):

ControlSelector Value
... ...
CT_PANTILT_ABSOLUTE_CONTROL 0x0D
... ...

又因?yàn)?code>value的值為控制選擇器CS(高字節(jié)),低字節(jié)為零。所以value的值應(yīng)該是0x0D << 8

index

Entity ID值為Camera Terminal指的是終端描述符的bTerminalID, 由于它屬于控制接口描述符的extra信息,所以還需要指的該接口的bInterfaceNunber:

val USB_DESC_TYPE_INTERFACE_LEN = 9.toByte()
val USB_DESC_TYPE_INTERFACE = 0x04.toByte()
val USB_DESC_TYPE_CS_INTERFACE = 0x24.toByte()
val USB_DESC_SUB_TYPE_VC_INPUT_TERMINAL = 0x02.toByte()
val USB_SUBCLASS_VIDEO_CONTROL = 0x01

private fun getPanTiltControlIndex(connection: UsbDeviceConnection): Int {
    val desc = connection.rawDescriptors ?: return -1

    var index = 0
    var isInVideoControlInterface = false
    var interfaceNumber = 0
    while (index < desc.size) {
        if (desc[index] == USB_DESC_TYPE_INTERFACE_LEN
            && desc[index + 1] == USB_DESC_TYPE_INTERFACE
            && desc[index + 5] == UsbConstants.USB_CLASS_VIDEO.toByte()
            && desc[index + 6] == USB_SUBCLASS_VIDEO_CONTROL.toByte()
        ) {
            // 找到bInterfaceClass為CC_VIDEO(0x0E) bInterfaceSubClass為SC_VIDEOCONTROL(0x01)的攝像頭控制接口的interfaceNumber
            isInVideoControlInterface = true
            interfaceNumber = desc[index + 2].toInt()
        } else if (isInVideoControlInterface) {
            if (desc[index + 1] != USB_DESC_TYPE_CS_INTERFACE) {
                return -1
            }
            if (desc[index + 2] == USB_DESC_SUB_TYPE_VC_INPUT_TERMINAL) {
                // 在攝像頭控制接口下找到bDescriptorType為CS_INTERFACE(0x24) bDescriptorSubtype為VC_INPUT_TERMINAL(0x02)的攝像頭終端描述符
                // 獲取它的bTerminalID用來(lái)和前面獲取到的攝像頭控制接口的interfaceNumber拼接成index
                return desc[index + 3].toInt().shl(8).or(interfaceNumber)
            }
        }

        index += desc[index]
    }
    return -1
}

request

我們要獲取的是當(dāng)前值所以request是GET_CUR(0x81),其他值的定如下:

名稱 說(shuō)明
RC_UNDEFINED 0x00 未定義
SET_CUR 0x01 設(shè)置屬性
GET_CUR 0x81 獲取當(dāng)前屬性
GET_MIN 0x82 獲取最小設(shè)置屬性
GET_MAX 0x83 獲取最大設(shè)置屬性
GET_RES 0x84 獲取分辨率屬性
GET_LEN 0x85 獲取數(shù)據(jù)長(zhǎng)度屬性
GET_INF 0x86 獲取設(shè)備支持的特定類請(qǐng)求屬性
GET_DEF 0x87 獲取默認(rèn)屬性

requestType

最后再來(lái)看requestType,由于index需要選擇的是實(shí)體ID(高字節(jié))、接口(低字節(jié))所以requestType應(yīng)該是10100001(接口或?qū)嶓w)。它的值這么奇怪是因?yàn)閞equestType的每個(gè)bit都是有意義的:

1.png

由于命令接受者為接口,所以我們?cè)诎l(fā)送控制指令前還是需要找到這個(gè)接口用claimInterface去鎖定它。

使用uvc控制指令的坑

似乎是因?yàn)槭褂冒沧康腃amera2等接口去讀取攝像頭畫面的時(shí)候會(huì)使用到這個(gè)控制接口,所以如果在預(yù)覽的時(shí)候去claimInterface鎖定它就會(huì)造成畫面卡死。

看起來(lái)似乎需要完全使用uvc自己從視頻流接口讀取畫面,而不能一半用uvc去控制攝像頭另一半用安卓原生api去獲取預(yù)覽畫面?;蛘哂萌∏傻姆椒ㄔ诎l(fā)送控制指令的時(shí)候先停止預(yù)覽,發(fā)送完再開(kāi)始。

其他三種傳輸

其他三種傳輸都是需要找到對(duì)應(yīng)的端點(diǎn)才能進(jìn)行通訊的,所以需要先獲取到端點(diǎn)信息.用UsbInterface.getEndpoint去遍歷接口下的端點(diǎn),然后判斷端點(diǎn)的類型和讀寫方向:

for (i in 0 until usbInterface.endpointCount) {
    val usbEndpoint = usbInterface.getEndpoint(i)
    when (usbEndpoint.type) {
        UsbConstants.USB_ENDPOINT_XFER_BULK -> {
            // 批量傳輸
            if (usbEndpoint.direction == UsbConstants.USB_DIR_OUT) {
                // 可寫入端點(diǎn)
            } else if (usbEndpoint.direction == UsbConstants.USB_DIR_IN) {
                // 可讀取端點(diǎn)
            }
        }
        UsbConstants.USB_ENDPOINT_XFER_ISOC -> {
            // 中斷傳輸
        }
        UsbConstants.USB_ENDPOINT_XFER_INT -> {
            // 同步傳輸
        }
    }
}

他們最終都是通過(guò)UsbDeviceConnection.bulkTransfer去調(diào)用的,例如可以先寫入請(qǐng)求在讀取響應(yīng):

val requestBuffer = ByteArray(256)
// 將數(shù)據(jù)保存到requestBuffer
// 然后往寫入端點(diǎn)寫入請(qǐng)求數(shù)據(jù)
connection.bulkTransfer(outPoint, sendBuff, sendBuff.size, timeout)

// 從讀取端點(diǎn)讀取響應(yīng)
val responseBuffer = ByteArray(256)
connection.bulkTransfer(inPoint, responseBuffer, responseBuffer.size, timeout)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書人閱讀 176,560評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,035評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,814評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,224評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,444評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,804評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,998評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,237評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,665評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,927評(píng)論 1 287
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,706評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,993評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容