摘自:https://www.jianshu.com/p/e522fa5798d2
libusb是一個提供USB設備訪問的跨平台用戶模式程序庫。該項目最新網址:http://www.libusb.info, 支持主流的操作系統:Linux、Mac OS X、 Windows、OpenBSD/NetBSD、Solaris、Haiku,支持USB 1.0到3.1的所有版本。
使用場景
從事軟件開發這么多年來好像還一直未遇到與usb設備相關的開發工作,直到這次開發刷機工具的過程中才有了這樣一個需求。軟件功能比較簡單,選擇好刷機文件檢測手機插入之后判斷手機當前處於何種狀態做相應的處理,針對刷機的具體處理暫且不表,手機插拔狀態的檢測成了我優先要解決的問題,采用adb和fastboot輪詢的方式當然也可以做到,但這樣就不夠優雅了,並且如果手機沒有開啟adb的時候也無法檢測到手機是否插入。libusb名聲在外,早些年其實已經知道它,但因為沒有使用它的需求所以也一直未認真了解過。
當然,對於我目前的需求來說,libusb的高級功能我也使用不到,僅僅使用了它的hotplug通知,所以這篇日志主要還是記錄下來本次使用libusb的經驗和遇到的坑。
相關API鏈接:http://libusb.sourceforge.net/api-1.0/group__hotplug.html
測試程序
#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; }
這幾年開發環境一直使用MacBook,編譯之后運行看起來一切順利。libusb號稱跨平台,因此擼起袖子就開始干了,然后就遇到了后面我要說的一些坑,如果你也有我類似的需求,並且希望讓程序跨平台運行,那么在選擇libusb的時候可以參考一下。運行結果:
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)
使用心得
-
利用vendorId和productId過濾目標設備。從測試程序中可以看出,在回調中通過 libusb_get_device_descriptor 獲取設備描述結構后,其成員idVendor和idProduct就是我們要的數據,比如我們刷機程序當前選擇的firmware支持某個廠商的某個型號手機,那么其它手機插入之后我們將自動過濾。我的作法簡單粗暴,有一個DeviceSpec類列出了支持的設備項,每個項目包含vendorId和productId,另外就是Android手機正常啟動狀態adb模式和bootloader下productId是不一樣的,我們可以通過這個區分adb模式和fastboot模式。
-
通過serialNumber來唯一標識設備。由於我的刷機工具支持同時對多台手機刷機,通過vendorId和productId只能對應同一型號設備,如何唯一標識每個設備我使用了serialNumber,如果你有更好的數據可以唯一標識設備請記得告訴我。由於程序中針對設備的操作都是異步的,因此有了唯一標識我才能在接下來針對設備的一系列操作中准確地維護各個設備的刷機狀態。
坑
簡單一個字『坑』才能形容我遇到這些坑的心情。
-
USB設備插入和拔除的回調我們能做的事是不一樣的。插入的回調中我們可以獲取到設備描述之后通過 libusb_open 打開USB設備,從而獲取到serialNumber,但是設備拔除之后的回調中 libusb_open 就沒辦法工作了,可是我們使用serialNumber作為設備唯一標識我們如何判斷拔除的到底是哪個設備?目前我只能使用笨辦法,維護一個插入設備的列表,拔除回調中遍歷當前所有設備再比較得出哪個設備被拔除了。如果你有更好的方法請告訴我,我這個做法實在是不優雅!
-
由於刷機過程需要重啟並且還會在正常啟動和bootloader兩種模式間切換,會觸發多次插入和拔除的回調,因此程序中維護設備列表時不能在拔除事件發生時簡單地從列表中移除,需要自行維護好設備的模式和狀態。
-
沒有深究過libusb源代碼,看起來回調應該是工作在同一個線程中,但實際上回調可能被同時執行。在我的程序中出現過這樣的情況,手機未開啟adb插入電腦時 usb_arrived_callback 被執行,開啟adb調試時 usb_left_callback 和 usb_arrived_callback 相繼被執行,這下問題來了,由於設備移除時需要遍歷當前所有設備,並且與我保存的列表對比才能知道哪個設備被移除,在執行 usb_left_callback 尚未結束的時候 usb_arrived_callback 就被調用了,這就導致了 usb_left_callback 遲於最后一次 usb_arrived_callback 執行結束,於是自己維護的設備狀態不對了,調試這個問題簡直讓人崩潰。由於本次項目我使用的是QT,因此在回調中使用了QT的信號來觸發,並且讓信號排隊處理,最終才把這個坑填上。
connect(this, SIGNAL(usbArriveSignal(libusb_device*)), this, SLOT(addDevice(libusb_device*)), Qt::QueuedConnection); connect(this, SIGNAL(usbLeftSignal()), this, SLOT(setLeftDeviceModes()), Qt::QueuedConnection);
-
最嚴重的坑來了,libusb在windows上不支持hotplug。當我在Mac下一切准備就緒轉到windows下准備編譯發布的時候真的崩潰了,注冊回調就失敗了,對比了一下返回值在頭文件中的定義才知道不支持,后來在github上才看到 關於這個問題的issue。看了一些網上關於windows平台上的USB插拔檢測的文章,本次工具使用的是QML,發現基本上沒有適合我的,目前在考慮使用libusbK解決windows平台上的問題,或許等我正式發布這個工具的時候libusb的新版本就解決了這個問題。
教訓
-
首次使用的第三方庫或者新的技術架構一定要充分地測試關鍵技術點,不要等到了正式產品開發階段才發現問題,這會導致整個產品技術架構的調整或者大大影響開發周期。
-
跨平台技術一定要在產品關鍵技術點上在各個平台上測試通過再進行正式產品的開發。