2014年4月9日 星期三

【Android】hardware/usb (UsbHost元件的使用心得)

雖然我只有使用了UsbHost系列的功能,UsbAccessory的部分還沒用過,但經驗相信是差不多,而且UsbHost部分的元件複雜多了。

要在這裡附上我使用的程式做範例是不太可能的,我只能盡量做到引用官方API,然後每個細節補充



要在Android上使用USB、或稱為OTG,需要使用Android建立在  android.hardware.usb  這個package下的一系列元件,還有在APP的AndroidManifest檔中寫下類似下面這樣的設定:

<manifest ...>
    <uses-feature android:name="android.hardware.usb.host" />
    <uses-sdk android:minSdkVersion="12" />
    ...
    <application>
        <activity ...>
            ...
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>

            <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>
    </application>
</manifest>
(希望進來看到這篇文的人看得懂怎麼修改Manifest,因為我討厭寫基本教學。)

特別注意一下:
android:resource="@xml/device_filter"
這行是要另外搭配一個XML檔,(在res目錄下直接新增一個目錄「xml」,然後在裡頭新增檔案,──有興趣的人也可以取不同的名字,只要不會造成混亂就好。)檔案的名稱就取為device_filter:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="1234" product-id="5678" class="255" subclass="66" protocol="1" />
</resources>
要特別注意的是這個範例有點「過於」詳盡,其實vendor-id/product-id/class/subclass/protocol....並不全是必須的。(像下面這段也是可以的。這些XML檔都是直接引用自官方API的教學內容。)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="1234" product-id="5678" />
</resources>

這些屬性/數值是每一台Usb裝置的「身分證」,我不是這方面的專家,無法非常精準正確的講述其中的細節,但是基本上......同一家公司生產的同一系列商品,基本上都有相同的vendor-id和product-id。

設定這個device-filter和Manifest,這樣裝置插上手機後,系統才會自動偵測「要不要啟動你的APP」。(也有不自動偵測,改走別的路徑的方式,就是自己設定BroadCastReciever去獲得裝置連線的Permission,但這方面的使用經驗還不多,改天再講吧。)

如果你/妳的APP要針對特定Usb裝置,就盡量把device_filter定義的詳細一點。如果不針對特定程式,就乾脆什麼都不要寫。


Android的Usb連線溝通方式,是以UsbDevice類別為基礎。每一個類別的物件代表一台Usb裝置。

獲得UsbDevice物件的方式,最簡單的就是......
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
但是這個方法在官網中的說明很模糊、很「曖昧」,像最基本的...這個intent是什麼?怎麼獲取?(根據後來的BroadCastReciever範例,這個Intent根本不是在Activity中獲得。)

所以我並沒有用它。



我用的是下面這段方法...(幾乎是官方API照抄。)

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
... //Android
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
while(deviceIterator.hasNext()){
    UsbDevice device = deviceIterator.next()
    //your code
}
但其實這段的作法是有很多彈性的...deviceList中的Key值其實就是UsbDevice的DeviceName。這段程式碼中獲得UsbDevice後,可以將DeviceName存起來,然後下次跑到這段時,直接使用DeviceName去獲得UsbDevice,就不用跑while迴圈了。

官方API教學程式碼如下。注意最後一行程式碼中的那個「"deviceName"」,這個地方並不是要使用者輸入「"deviceName"」,而是要輸入上一面的DeviceName!

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
...  HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
UsbDevice device = deviceList.get("deviceName");


菜鳥、初學Android的人可能會被API這種寫法給害死,就傻傻的一直輸入「"deviceName"」,然後好奇為何HashMap永遠要不到資料。因為直覺沒有敏銳到發現「我幹嘛在一個HashMap的get()中輸入deviceName這個字?想也知道這地方的內容是彈性的!

雖然Key值可以直接請HashMap吐給你/妳,可是用OTG時,是沒有adb功能的,(因為Usb接頭接著Usb裝置而不是電腦,)又某種原因,wifi-adb根本靠不住,(官方推薦改用wifi輸出Log資訊到電腦上,)所以吐出來的Key值...作夢想想就好。(或是自己在自己的APP中設置背景資訊的輸出功能,等到開發完畢在關閉這個功能就好。)

(因為Manifest中加了intent-filter,所以不需要經由BroadCastReciever去獲得Permission,因此我這裡不說明官方API中那段的使用方式。)



接下來是直接跟Usb機器「溝通」的部分。

跟機器「溝通」實在是個很模糊的概念,因為最終都是「送出Byte陣列資料」。

譬如...這裡有段Byte矩陣要送進特殊的隨身碟中存起來,只要矩陣開頭和結尾的Byte值是「100,101」,隨身碟就會把(「100,101」捨棄後)中間的值存起來。

(或是個喇叭,會把中間的值轉成音效,然後發出噪音。)

搞不懂的人可能要去研究一下網路傳輸或I/O原理,或就單純的自己用Java架個簡單的測試伺服器,然後練習用手機跟這個伺服器溝通看看。

就先......假設看到這裡的人都懂怎麼作,(遙遠的未來,或許我也來寫寫I/O相關的心得文吧。)



這段程式碼就是傳輸和接收資料的方法。(注意!Android的Usb元件能夠傳送和接收的資料有長度限制。上限是16K。)
private Byte[] bytes //要傳輸的資料  特別注意的是這個bytes的長度不能超過16K 也就是 16 * 1024  因為這是它每次能傳輸的資料量極限 
private static int TIMEOUT = 0; //每次傳輸的等待時間 (如果Usb機器超過這個時間沒回傳  就視為傳輸失敗)
private boolean forceClaim = true;
...
UsbInterface intf = device.getInterface(0);
UsbEndpoint endpoint = intf.getEndpoint(0);//又是個API害死人的例子
UsbDeviceConnection connection = mUsbManager.openDevice(device); 
connection.claimInterface(intf, forceClaim);
connection.bulkTransfer(endpoint, bytes, bytes.length, TIMEOUT); //do in another thread
最後一行connection.bulkTransfer中,這個用法跟使用File元件讀取檔案的使用方法很像。(所以細節我也不說明了。)

關鍵在於每個UsbInterface中都會有兩組UsbEndpoint,一組為傳輸、一組為接收。(哪組在先、哪組在後,似乎並沒有絕對的標準和規則。所以我在上面的程式碼中補註明了「又是個API害死人的例子」。)

UsbConstants(這是一個UsbHost的元件)中可以看到相關的參數UsbConstants.USB_DIR_IN/UsbConstants.USB_DIR_OUT。

去判讀每組Endpoint的Direction,(使用指令getDirection(),可以得到參數,判讀參數是IN或OUT,就可以知道Endpoint的屬性和功能。)把傳輸的丟進去就會傳輸資料給Usb機器,把接收的丟進去就會從Usb機器接收資料。



最後,官方文件輕輕帶過、但困擾了我整整一個月的部分.......

UsbDeviceConnection和UsbInterface元件使用完,要進行「釋放」和「關閉」的動作。

為何要特別強調?

因為這段方式沒有「標準作法」!

一般人會以為「我不再需要使用Usb功能了,就把它關閉,」.......錯!這些元件雖然基本上是ParceLable介面的實作,(跟Bundle一樣,)但其實它們都混合了JNI,所以經常資料傳輸到一半,這兩個元件(其中一個?或兩個同時發生?)會變成「Null」!(我的解釋是「它自己把自己回收掉了。)

碰到這種情況時,就要重作一次獲取的動作。

可是隨著這種情況發生、多次獲取後,經常會連獲取都失敗!(UsbManager的openDevice指令回傳Null?UsbDevice的getInterface也回傳Null?)

因為UsbManager用一組「很奇特」的HashMap來存放這些元件的資料,這組HashMap功能異常(資料不見?資料錯誤?存放資料總數超過上限?)就會回傳Null。(可以用IDE打開UsbManager,但對於理解HashMap會異常的原因還是沒有幫助,而且修改SDK的程式碼對未來APP在實機上的運作其實沒什麼幫助。)

所以不能等到整個功能都用完了才去作釋放動作,必須要定時的釋放。

「每次傳輸/接收完,都釋放一次?」......還是錯!

這樣只是把「獲取失敗」發生的時間往後推遲而以。(我清清楚楚的記得...從不釋放,程式會在第540~550之間轉為「獲取失敗」。每次釋放,程式會在第1050~1100之間轉為「獲取失敗」。非常穩定、且準時的兩組數字。)



我的設定是「傳輸+接收」的次數「每十次」就進行一次釋放、再獲取的動作。

但這個數字是我碰運氣、靠靈感隨便決定的,(當然也在平版實機上進行了無數次的測試。)

如果你/妳要使用Android的Usb連線功能,而且不是用在最常見、制式標準的隨身碟這類儲存工具上,最後這個步驟記得多花點時間測試。

沒有留言:

張貼留言