使用libusb檢測(cè)USB設(shè)備插拔狀態(tài)

libusb是一個(gè)提供USB設(shè)備訪問(wèn)的跨平臺(tái)用戶模式程序庫(kù)。該項(xiàng)目最新網(wǎng)址:http://www.libusb.info, 支持主流的操作系統(tǒng):Linux、Mac OS X、 Windows、OpenBSD/NetBSD、Solaris、Haiku,支持USB 1.0到3.1的所有版本。

使用場(chǎng)景

從事軟件開(kāi)發(fā)這么多年來(lái)好像還一直未遇到與usb設(shè)備相關(guān)的開(kāi)發(fā)工作,直到這次開(kāi)發(fā)刷機(jī)工具的過(guò)程中才有了這樣一個(gè)需求。軟件功能比較簡(jiǎn)單,選擇好刷機(jī)文件檢測(cè)手機(jī)插入之后判斷手機(jī)當(dāng)前處于何種狀態(tài)做相應(yīng)的處理,針對(duì)刷機(jī)的具體處理暫且不表,手機(jī)插拔狀態(tài)的檢測(cè)成了我優(yōu)先要解決的問(wèn)題,采用adb和fastboot輪詢的方式當(dāng)然也可以做到,但這樣就不夠優(yōu)雅了,并且如果手機(jī)沒(méi)有開(kāi)啟adb的時(shí)候也無(wú)法檢測(cè)到手機(jī)是否插入。libusb名聲在外,早些年其實(shí)已經(jīng)知道它,但因?yàn)闆](méi)有使用它的需求所以也一直未認(rèn)真了解過(guò)。

當(dāng)然,對(duì)于我目前的需求來(lái)說(shuō),libusb的高級(jí)功能我也使用不到,僅僅使用了它的hotplug通知,所以這篇日志主要還是記錄下來(lái)本次使用libusb的經(jīng)驗(yàn)和遇到的坑。

相關(guān)API鏈接:http://libusb.sourceforge.net/api-1.0/group__hotplug.html

測(cè)試程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <libusb-1.0/libusb.h>

static int LIBUSB_CALL
usb_arrived_callback(struct libusb_context *ctx, struct libusb_device *dev,
    libusb_hotplug_event event, void *userdata)
{
    struct libusb_device_handle *handle;
    struct libusb_device_descriptor desc;
    unsigned char buf[512];
    int rc;

    libusb_get_device_descriptor(dev, &desc);
    printf("Add usb device: \n");
    printf("\tCLASS(0x%x) SUBCLASS(0x%x) PROTOCOL(0x%x)\n",
        desc.bDeviceClass, desc.bDeviceSubClass, desc.bDeviceProtocol);
    printf("\tVENDOR(0x%x) PRODUCT(0x%x)\n", desc.idVendor, desc.idProduct);
    rc = libusb_open(dev, &handle);
    if (LIBUSB_SUCCESS != rc) {
        printf("Could not open USB device\n");
        return 0;
    }

    memset(buf, 0, sizeof(buf));
    rc = libusb_get_string_descriptor_ascii(handle, desc.iManufacturer, buf, sizeof(buf));
    if (rc < 0) {
        printf("Get Manufacturer failed\n");
    } else {
        printf("\tManufacturer: %s\n", buf);
    }

    memset(buf, 0, sizeof(buf));
    rc = libusb_get_string_descriptor_ascii(handle, desc.iProduct, buf, sizeof(buf));
    if (rc < 0) {
        printf("Get Product failed\n");
    } else {
        printf("\tProduct: %s\n", buf);
    }

    memset(buf, 0, sizeof(buf));
    rc = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, buf, sizeof(buf));
    if (rc < 0) {
        printf("Get SerialNumber failed\n");
    } else {
        printf("\tSerialNumber: %s\n", buf);
    }
    libusb_close(handle);

    return 0;
}

static int LIBUSB_CALL
usb_left_callback(struct libusb_context *ctx, struct libusb_device *dev,
    libusb_hotplug_event event, void *userdata)
{
    struct libusb_device_descriptor desc;

    libusb_get_device_descriptor(dev, &desc);
    printf("Remove usb device: CLASS(0x%x) SUBCLASS(0x%x) iSerialNumber(0x%x)\n",
        desc.bDeviceClass, desc.bDeviceSubClass, desc.iSerialNumber);

    return 0;
}

int main(int argc, char **argv)
{
    libusb_hotplug_callback_handle usb_arrived_handle;
    libusb_hotplug_callback_handle usb_left_handle;
    libusb_context *ctx;
    int rc;

    libusb_init(&ctx);
    rc = libusb_hotplug_register_callback(ctx, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
        LIBUSB_HOTPLUG_NO_FLAGS, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY,
        LIBUSB_HOTPLUG_MATCH_ANY, usb_arrived_callback, NULL, &usb_arrived_handle);
    if (LIBUSB_SUCCESS != rc) {
        printf("Error to register usb arrived callback\n");
        goto failure;
    }

    rc = libusb_hotplug_register_callback(ctx, LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
        LIBUSB_HOTPLUG_NO_FLAGS, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY,
        LIBUSB_HOTPLUG_MATCH_ANY, usb_left_callback, NULL, &usb_left_handle);
    if (LIBUSB_SUCCESS != rc) {
        printf("Error to register usb left callback\n");
        goto failure;
    }

    while (1) {
        libusb_handle_events_completed(ctx, NULL);
        usleep(1000);
    }

    libusb_hotplug_deregister_callback(ctx, usb_arrived_handle);
    libusb_hotplug_deregister_callback(ctx, usb_left_handle);
    libusb_exit(ctx);

    return 0;

failure:
    libusb_exit(ctx);
    return EXIT_FAILURE;
}

這幾年開(kāi)發(fā)環(huán)境一直使用MacBook,編譯之后運(yùn)行看起來(lái)一切順利。libusb號(hào)稱跨平臺(tái),因此擼起袖子就開(kāi)始干了,然后就遇到了后面我要說(shuō)的一些坑,如果你也有我類似的需求,并且希望讓程序跨平臺(tái)運(yùn)行,那么在選擇libusb的時(shí)候可以參考一下。運(yùn)行結(jié)果:

lidroid@lidroid-MacBook-Pro ~/libusb-test $ ./test
Add usb device:
    CLASS(0x0) SUBCLASS(0x0) PROTOCOL(0x0)
    VENDOR(0x18d1) PRODUCT(0x4ee2)
    Manufacturer: Huawei
    Product: Nexus 6P
    SerialNumber: CVH7N15B10001899
Remove usb device: CLASS(0x0) SUBCLASS(0x0) iSerialNumber(0x3)

使用心得

  1. 利用vendorId和productId過(guò)濾目標(biāo)設(shè)備。從測(cè)試程序中可以看出,在回調(diào)中通過(guò) libusb_get_device_descriptor 獲取設(shè)備描述結(jié)構(gòu)后,其成員idVendor和idProduct就是我們要的數(shù)據(jù),比如我們刷機(jī)程序當(dāng)前選擇的firmware支持某個(gè)廠商的某個(gè)型號(hào)手機(jī),那么其它手機(jī)插入之后我們將自動(dòng)過(guò)濾。我的作法簡(jiǎn)單粗暴,有一個(gè)DeviceSpec類列出了支持的設(shè)備項(xiàng),每個(gè)項(xiàng)目包含vendorId和productId,另外就是Android手機(jī)正常啟動(dòng)狀態(tài)adb模式和bootloader下productId是不一樣的,我們可以通過(guò)這個(gè)區(qū)分adb模式和fastboot模式。

  2. 通過(guò)serialNumber來(lái)唯一標(biāo)識(shí)設(shè)備。由于我的刷機(jī)工具支持同時(shí)對(duì)多臺(tái)手機(jī)刷機(jī),通過(guò)vendorId和productId只能對(duì)應(yīng)同一型號(hào)設(shè)備,如何唯一標(biāo)識(shí)每個(gè)設(shè)備我使用了serialNumber,如果你有更好的數(shù)據(jù)可以唯一標(biāo)識(shí)設(shè)備請(qǐng)記得告訴我。由于程序中針對(duì)設(shè)備的操作都是異步的,因此有了唯一標(biāo)識(shí)我才能在接下來(lái)針對(duì)設(shè)備的一系列操作中準(zhǔn)確地維護(hù)各個(gè)設(shè)備的刷機(jī)狀態(tài)。

簡(jiǎn)單一個(gè)字『坑』才能形容我遇到這些坑的心情。

  1. USB設(shè)備插入和拔除的回調(diào)我們能做的事是不一樣的。插入的回調(diào)中我們可以獲取到設(shè)備描述之后通過(guò) libusb_open 打開(kāi)USB設(shè)備,從而獲取到serialNumber,但是設(shè)備拔除之后的回調(diào)中 libusb_open 就沒(méi)辦法工作了,可是我們使用serialNumber作為設(shè)備唯一標(biāo)識(shí)我們?nèi)绾闻袛喟纬牡降资悄膫€(gè)設(shè)備?目前我只能使用笨辦法,維護(hù)一個(gè)插入設(shè)備的列表,拔除回調(diào)中遍歷當(dāng)前所有設(shè)備再比較得出哪個(gè)設(shè)備被拔除了。如果你有更好的方法請(qǐng)告訴我,我這個(gè)做法實(shí)在是不優(yōu)雅!

  2. 由于刷機(jī)過(guò)程需要重啟并且還會(huì)在正常啟動(dòng)和bootloader兩種模式間切換,會(huì)觸發(fā)多次插入和拔除的回調(diào),因此程序中維護(hù)設(shè)備列表時(shí)不能在拔除事件發(fā)生時(shí)簡(jiǎn)單地從列表中移除,需要自行維護(hù)好設(shè)備的模式和狀態(tài)。

  3. 沒(méi)有深究過(guò)libusb源代碼,看起來(lái)回調(diào)應(yīng)該是工作在同一個(gè)線程中,但實(shí)際上回調(diào)可能被同時(shí)執(zhí)行。在我的程序中出現(xiàn)過(guò)這樣的情況,手機(jī)未開(kāi)啟adb插入電腦時(shí) usb_arrived_callback 被執(zhí)行,開(kāi)啟adb調(diào)試時(shí) usb_left_callbackusb_arrived_callback 相繼被執(zhí)行,這下問(wèn)題來(lái)了,由于設(shè)備移除時(shí)需要遍歷當(dāng)前所有設(shè)備,并且與我保存的列表對(duì)比才能知道哪個(gè)設(shè)備被移除,在執(zhí)行 usb_left_callback 尚未結(jié)束的時(shí)候 usb_arrived_callback 就被調(diào)用了,這就導(dǎo)致了 usb_left_callback 遲于最后一次 usb_arrived_callback 執(zhí)行結(jié)束,于是自己維護(hù)的設(shè)備狀態(tài)不對(duì)了,調(diào)試這個(gè)問(wèn)題簡(jiǎn)直讓人崩潰。由于本次項(xiàng)目我使用的是QT,因此在回調(diào)中使用了QT的信號(hào)來(lái)觸發(fā),并且讓信號(hào)排隊(duì)處理,最終才把這個(gè)坑填上。
    connect(this, SIGNAL(usbArriveSignal(libusb_device*)), this, SLOT(addDevice(libusb_device*)), Qt::QueuedConnection); connect(this, SIGNAL(usbLeftSignal()), this, SLOT(setLeftDeviceModes()), Qt::QueuedConnection);

  4. 最嚴(yán)重的坑來(lái)了,libusb在windows上不支持hotplug。當(dāng)我在Mac下一切準(zhǔn)備就緒轉(zhuǎn)到windows下準(zhǔn)備編譯發(fā)布的時(shí)候真的崩潰了,注冊(cè)回調(diào)就失敗了,對(duì)比了一下返回值在頭文件中的定義才知道不支持,后來(lái)在github上才看到 關(guān)于這個(gè)問(wèn)題的issue??戳艘恍┚W(wǎng)上關(guān)于windows平臺(tái)上的USB插拔檢測(cè)的文章,本次工具使用的是QML,發(fā)現(xiàn)基本上沒(méi)有適合我的,目前在考慮使用libusbK解決windows平臺(tái)上的問(wèn)題,或許等我正式發(fā)布這個(gè)工具的時(shí)候libusb的新版本就解決了這個(gè)問(wèn)題。

教訓(xùn)

  • 首次使用的第三方庫(kù)或者新的技術(shù)架構(gòu)一定要充分地測(cè)試關(guān)鍵技術(shù)點(diǎn),不要等到了正式產(chǎn)品開(kāi)發(fā)階段才發(fā)現(xiàn)問(wèn)題,這會(huì)導(dǎo)致整個(gè)產(chǎn)品技術(shù)架構(gòu)的調(diào)整或者大大影響開(kāi)發(fā)周期。

  • 跨平臺(tái)技術(shù)一定要在產(chǎn)品關(guān)鍵技術(shù)點(diǎn)上在各個(gè)平臺(tái)上測(cè)試通過(guò)再進(jìn)行正式產(chǎn)品的開(kāi)發(fā)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容