使用libusb检测USB设备插拔状态
摘自: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的新版本就解决了这个问题。
教训
-
首次使用的第三方库或者新的技术架构一定要充分地测试关键技术点,不要等到了正式产品开发阶段才发现问题,这会导致整个产品技术架构的调整或者大大影响开发周期。
-
跨平台技术一定要在产品关键技术点上在各个平台上测试通过再进行正式产品的开发。