前言與背景
一般來說,不管是在什么平臺上需要與外接硬件交互,第一件事都是應該能夠正確的識別出目標硬件。
例如在 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 :
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 基本可以完成對連接設備的判斷了。
在安卓中可以通過以下代碼獲取所有已連接設備的名稱:
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 用于獲取唯一設備的代碼:
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 >= 12sb.append(device.getProductId()); sb.append("#"); // API >= 12sb.append(device.getDeviceClass()); sb.append("#"); // API >= 12sb.append(device.getDeviceSubclass()); sb.append("#"); // API >= 12sb.append(device.getDeviceProtocol()); // API >= 12if (!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 >= 21sb.append(device.getConfigurationCount()); sb.append("#"); // API >= 21if (BuildCheck.isMarshmallow()) {sb.append(device.getVersion()); sb.append("#"); // API >= 23}}return sb.toString();
}
按道理來說,至此本文就應該結束了,但是,但是又來了。
低效但最可靠的方式
在之前,我一直都是配合著上面兩種方法做識別的處理,也一直沒有出過什么意外。
因為之前我們的安卓終端是定制設備,外接硬件也是固定的幾個硬件設備,所以對于設備識別倒也還算好處理。
但是,就怕哪天老板腦洞大開,給我們整點花活,沒錯,我的老板就開腦洞了。
他覺得定制終端不是很方便,所以想直接使用用戶自己的普通手機來完成我們的業務流程。
一開始倒也沒什么大問題,我依舊按照定制終端的寫法寫了程序,剛開始運行也確實沒有什么問題。
但是,某天某同事氣沖沖的找到我,質問我寫的什么玩意兒,他怒吼到:我手機明明什么東西都沒插,你為什么說我插上了 RFID 讀卡器?現在我真的插上了讀卡器卻什么也讀不出來了!
好家伙,給我說的一愣一愣的,趕緊借他手機過來檢查了一下,檢查結果又給我看的一愣一愣的。
我在程序中寫入的這個讀卡器的 ID,居然和他手機某個傳感器的 ID 重復了!
因為使用的這款讀卡器是新采購的,且沒有寫入產品名稱,所以我也偷懶沒有對產品名稱做校驗,只校驗了 ID,但是令我萬萬沒想到的是,手機在沒有插入任何外設的情況下居然也能讀到 USB 信息,一下子我已經不知道這個 USB 信息是從哪兒來的了。
不過這個問題也只是一個小插曲,我把產品名的判斷加上就行了。
真正讓我頭痛的是后面的事情。
后來老板又覺得我們自己采購傳感器做外設不太好,想直接連接成熟的成品讀它們的數據就行了。
比如之前我們業務有需要稱重的地方是使用自己采購的天平傳感器自己設計硬件來讀取數據的,現在需要改為直接連接市面上成熟的成品電子秤,從其中讀取數據來使用。
那就接唄,初期擬定的是支持市占率最高的某兩款不同型號的成品電子秤,在老板買來樣品后,我一插上安卓設備就傻了。
熟悉的 Vid Pid 完全一致,行唄,我再拿產品名唄,一看……我人傻了,怎么連產品名都一摸一樣啊!
這又是別人的成品自然不可能更改產品名,那咋辦?兩款秤使用的通信協議也不一樣,所以必須做出區分才能正確的讀出數據。
此時,如果想區分出當前連接的到底是哪款秤只有一個低效但是很有用的辦法了,那就是使用兩款秤的協議挨個發送請求,哪個有回復就說明現在連接的是哪款秤。
這就要求我們和秤的協議必須有一個不會破壞兩個設備正常運行的“無害”指令以及這個指令必須要能有一個可分辨的正常回復,比如讀取秤的版本號之類的就是理想的選擇。
否則有時某些設備雖然協議不對,但是在收到指令后還是會回復一些亂碼之類的,此時如果只是根據是否能收到回復來判斷是否是特定設備也是很容易出錯的。
另外一種情況就是,如果設備收到的是不支持的指令,則不會回復任何消息,此時我們只有通過等待連接是否超時才能判斷是否是特定設備了。
這樣的話,如果當前需要支持的設備比較少還好,如果后期需要支持的設備特別多的話,就意味著等待判斷設備型號就需要非常長的一段時間了。
這就帶來了最后一種終極解決方案,也是我目前采用的方案,那就是讓用戶自己去選擇他連接的是哪個設備。
在我們使用了上述的方案判斷設備后依舊無法完全判斷是否是特定的設備時,我們就將選擇權交給用戶,由用戶自己來確定連接的是哪個設備。
參考資料
- Device identification strings
- usb_vids
- How to identify the correct USB Device in LabVIEW