前言与背景
一般来说,不管是在什么平台上需要与外接硬件交互,第一件事都是应该能够正确的识别出目标硬件。
例如在 Windows 上,当一个新的外设设备被插入到我们的电脑时,系统会通过 Hardware IDs 、Compatible IDs 来确定连接的是什么设备并为其选择或安装一个合适的驱动程序以供后续使用。
在获取到可用的驱动程序后 Windows 还会使用 Instance IDs 、 Device instance IDs 用于标识设备的唯一性。
同理,在我们安卓上与外接硬件设备通信之前我们首先要做的应该是正确的识别出哪个设备是我们需要与之交互的设备。
在之前的文章中,我们说过在安卓端有两种连接串口的方式,一种是使用 android-serialport-api,即 root 后直接读写 /dev/ttys
文件;另外一种则是不需要 root,使用安卓提供的 USB HOST 模式通过 USB API 与连接的 USB 设备通信。
对于第一种情况,我目前遇到的都是每个 /dev/ttys
路径对应的都是一个真实的物理接口,而且这个物理接口一般都是主板直连的 串口 而非 USB 转接口。换言之,哪个接口连的是哪个设备可以认为是固定的,只需要直接通过接口来识别即可,所以这种情况我们暂时不讨论。
对于第二种情况,一般来说也是和第一种情况类似,连接的设备基本都是固定的设备,所以要识别还是很简单的。
但是偏偏就是有不一般的时候,且听我慢慢道来。
通用的识别方式
使用 USB HOST 模式和串口设备通信的话,就需要遵循 USB 标准。
根据 USB 标准,每个 USB 设备都会有一个 供应商 ID(vendorId, Vid) 和 产品 ID(productId, Pid)。
它们都是一串 16 位的数字(两字节),用于向其他主机设备标识当前设备。
供应商 ID 是由 USB Implementers Forum 分配给特定的公司,也就是说同一家公司出的产品 Vid 都是一样的。
产品 ID 由生产公司自行分配,一般来说是同一型号的产品使用同一个 Pid 。
这两个 ID 是写死到硬件设备中的,并且会在设备插入时与描述信息和产品信息以及有关设备支持的通信协议附加信息一起发送给主机设备。
一般来说,只要确定了 Vid 和 Pid 基本就可以完全确定当前连接的设备。
在安卓端可以通过如下方式读取到当前已连接的所有设备的这两个 ID :
kotlin
val usbManager = contex.getSystemService(Context.USB_SERVICE) as UsbManager
val deviceList = usbManager.deviceList
var deviceId = ""
for ((_, v) in deviceList) {
deviceId += "productId=${v.productId}\nvendorId=${v.vendorId}\n===========\n"
}
println(deviceId)
但是,上面也说了,只有在一般情况下这个方法会好使,因为理想很丰满,现实很残酷。
每个供应商如果想要获取到 USB Implementers Forum 分配的 Vid 则需要缴纳 5000 美元的会员费,这就导致许多硬件厂商为了节约成本就不会去申请自己的 Vid。
但是没有 Vid 显然也不行啊,总不能自己瞎写一个吧。
于是,"聪明的"厂商们就动起了歪脑筋,那我不写我自己的 ID 不就得了,直接使用底层通信芯片的 ID 岂不是完美?
因为一般硬件厂商在做自己的硬件设备时底层通信使用的不是 USB 通信,在最终发布产品时为了增加 USB 支持会在产品中内嵌一个 USB 转换芯片,使其支持 USB 通信,所以它们才能直接使用 USB 转换芯片的 ID。
即使这是被 USB 标准所禁止的行为,但是为了节约成本,厂商们才不会管这么多呢。
这就导致出现了很多明明是两个八竿子打不着的厂商出的两个完全没有任何关联的硬件设备, Vid 和 Pid 却是一模一样的。
我在工作中就遇到过这种情况,我司的某个安卓终端需要连接两个外接传感器设备,一个电子天平和一个 RFID 传感器,乍一看,这俩完全没有关联是吧,然而,它们的 productId 都是 29987 ,vendorId 都是 6790 ......
上面我们说过,Vid 是 USB 组织分配的,所以我们可以查的到这个 ID 对应的是哪家厂家:
(数据来自参考资料 2)
搜索这家公司的官网,并在其中查找这个产品 ID ,额,行吧,官网手册没有给出产品 ID,只有产品型号,但是无妨,我们可以在 这里 查询:
其实从型号名字已经能看出了,这确实是一个串口转 USB 芯片的 ID,但是严谨起见,我们还是在它官网搜一下这个型号:
这下确实证实了,这是一块串口转 USB 芯片。也就是说,这两个传感器都使用了同一家公司的同一型号的转换芯片,并且它们都没有买供应商 ID,所以用的都是转换芯片的 ID。
那么,对于这种情况,我们该如何分辨谁是谁?
配合其他辅助信息来识别
在上一节,我们说到由于大多数厂商都不会去遵循 USB 标准,所以单独靠 Vid 和 Pid 是无法区分出连接的设备的。
对于这种情况,我们可能还需要使用其他的辅助判断手段。
在 USB 标准中对于设备的描述信息还有一个叫 productName 的字段。
这个字段也是由设备厂商自行写入的,且最重要的是这个字段不是写死在硬件中的,是可修改的。
所以,我们可以要求厂商在出厂时将这个字段改成我们指定的名称或者我们也可以自行使用软件(可以找厂商要修改软件)将其改为指定的名称。
如此一来,Vid+Pid+productName 基本可以完成对连接设备的判断了。
在安卓中可以通过以下代码获取所有已连接设备的名称:
kotlin
val usbManager = contex.getSystemService(Context.USB_SERVICE) as UsbManager
val deviceList = usbManager.deviceList
var deviceName = ""
for ((_, v) in deviceList) {
deviceName += "productName=${v.productName}\n===========\n"
}
println(deviceName)
另外,如果还是不放心的话,大可直接把 USB 描述信息中的所有信息都加上做一个 Hash,然后用来做判断。
只是这种方法只适合用于判断唯一设备而不适合于用来判断同一型号的不同设备,因为 USB 描述信息中有些信息并不是同一个型号就都是一样的。
这里就直接给大伙看看 UVCCamera 用于获取唯一设备的代码:
java
public static final String getDeviceKeyName(final UsbDevice device, final String serial, final boolean useNewAPI) {
if (device == null) return "";
final StringBuilder sb = new StringBuilder();
sb.append(device.getVendorId()); sb.append("#"); // API >= 12
sb.append(device.getProductId()); sb.append("#"); // API >= 12
sb.append(device.getDeviceClass()); sb.append("#"); // API >= 12
sb.append(device.getDeviceSubclass()); sb.append("#"); // API >= 12
sb.append(device.getDeviceProtocol()); // API >= 12
if (!TextUtils.isEmpty(serial)) {
sb.append("#"); sb.append(serial);
}
if (useNewAPI && BuildCheck.isAndroid5()) {
sb.append("#");
if (TextUtils.isEmpty(serial)) {
// ANDROID 10 以上会获取不到 SerialNumber , 会报错:SecurityException ,无权限读取该信息
// 不过此处获取串口序列号仅用于计算设备的哈希值作为保存关于此设备的配置信息的 KEY
// 包括是否申请了权限的这个信息,所以意味着会在申请权限前调用这个方法
// 所以此处可以不做特殊处理,如果获取不到就不获取了,其他信息已经足以计算出这个设备的唯一哈希
try {
sb.append(device.getSerialNumber()); sb.append("#"); // API >= 21
} catch (SecurityException e) {
LogUtil.e(TAG, "getDeviceKeyName: 获取串口序列号失败", e);
}
}
sb.append(device.getManufacturerName()); sb.append("#"); // API >= 21
sb.append(device.getConfigurationCount()); sb.append("#"); // API >= 21
if (BuildCheck.isMarshmallow()) {
sb.append(device.getVersion()); sb.append("#"); // API >= 23
}
}
return sb.toString();
}
按道理来说,至此本文就应该结束了,但是,但是又来了。
低效但最可靠的方式
在之前,我一直都是配合着上面两种方法做识别的处理,也一直没有出过什么意外。
因为之前我们的安卓终端是定制设备,外接硬件也是固定的几个硬件设备,所以对于设备识别倒也还算好处理。
但是,就怕哪天老板脑洞大开,给我们整点花活,没错,我的老板就开脑洞了。
他觉得定制终端不是很方便,所以想直接使用用户自己的普通手机来完成我们的业务流程。
一开始倒也没什么大问题,我依旧按照定制终端的写法写了程序,刚开始运行也确实没有什么问题。
但是,某天某同事气冲冲的找到我,质问我写的什么玩意儿,他怒吼到:我手机明明什么东西都没插,你为什么说我插上了 RFID 读卡器?现在我真的插上了读卡器却什么也读不出来了!
好家伙,给我说的一愣一愣的,赶紧借他手机过来检查了一下,检查结果又给我看的一愣一愣的。
我在程序中写入的这个读卡器的 ID,居然和他手机某个传感器的 ID 重复了!
因为使用的这款读卡器是新采购的,且没有写入产品名称,所以我也偷懒没有对产品名称做校验,只校验了 ID,但是令我万万没想到的是,手机在没有插入任何外设的情况下居然也能读到 USB 信息,一下子我已经不知道这个 USB 信息是从哪儿来的了。
不过这个问题也只是一个小插曲,我把产品名的判断加上就行了。
真正让我头痛的是后面的事情。
后来老板又觉得我们自己采购传感器做外设不太好,想直接连接成熟的成品读它们的数据就行了。
比如之前我们业务有需要称重的地方是使用自己采购的天平传感器自己设计硬件来读取数据的,现在需要改为直接连接市面上成熟的成品电子秤,从其中读取数据来使用。
那就接呗,初期拟定的是支持市占率最高的某两款不同型号的成品电子秤,在老板买来样品后,我一插上安卓设备就傻了。
熟悉的 Vid Pid 完全一致,行呗,我再拿产品名呗,一看......我人傻了,怎么连产品名都一摸一样啊!
这又是别人的成品自然不可能更改产品名,那咋办?两款秤使用的通信协议也不一样,所以必须做出区分才能正确的读出数据。
此时,如果想区分出当前连接的到底是哪款秤只有一个低效但是很有用的办法了,那就是使用两款秤的协议挨个发送请求,哪个有回复就说明现在连接的是哪款秤。
这就要求我们和秤的协议必须有一个不会破坏两个设备正常运行的"无害"指令以及这个指令必须要能有一个可分辨的正常回复,比如读取秤的版本号之类的就是理想的选择。
否则有时某些设备虽然协议不对,但是在收到指令后还是会回复一些乱码之类的,此时如果只是根据是否能收到回复来判断是否是特定设备也是很容易出错的。
另外一种情况就是,如果设备收到的是不支持的指令,则不会回复任何消息,此时我们只有通过等待连接是否超时才能判断是否是特定设备了。
这样的话,如果当前需要支持的设备比较少还好,如果后期需要支持的设备特别多的话,就意味着等待判断设备型号就需要非常长的一段时间了。
这就带来了最后一种终极解决方案,也是我目前采用的方案,那就是让用户自己去选择他连接的是哪个设备。
在我们使用了上述的方案判断设备后依旧无法完全判断是否是特定的设备时,我们就将选择权交给用户,由用户自己来确定连接的是哪个设备。