本文的目的是提供關於無線藍牙技術 Java API(即 JSR-82 API)的實踐體驗。如果您不熟悉藍牙語 義,不要擔心。我將在藍牙協議簡介及其用例(稱為藍牙模式)中介紹這些內容。因為本應用程序將展示 如何使用藍牙技術向其他藍牙設備傳輸圖像,所以還將展示如何使用 JSR-75 的 File Connection API 以程序的方式對移動設備進行訪問。在本文結束時,將獲得能夠向遠程藍牙設備傳輸圖像的指導性示例( 及相關文件)。
藍牙協議
關於藍牙的一個鮮為人知的事實是:它即便不是世界上配置最為廣泛並且最成功的 SOA(面向服務架 構)系統,那麼也是其中之一。藍牙技術得到廣泛的安裝采用(部署的設備超過 5 億台),並且當前的 數據估計每周都有另外五百萬台藍牙設備送出。遠在“面向服務架構”成為專門術語之前,藍牙協議就已 經提供了服務注冊、服務發現和服務調用機制。
因此,藍牙協議結合了面向服務架構並采用 HTTP 和 FTP 之類的其他協議中熟悉使用的客戶端/服務 器通信架構:在客戶端發出請求之前,服務器耐心地等待。當前市場上的藍牙設備能夠以 3 Mb/s 的速率 進行通信,並且可以支持立體聲無線音頻。以下圖 1 顯示了藍牙協議棧的各個層。
圖 1:藍牙協議棧及其層
因為本文的重點是 OBEX,所以我沒有講述圖 1 中所有層的細節,但是我確實希望提供關於主要的支 持 OBEX 層的一些詳細信息。如您所見,該棧的主要協議層之一是 L2CAP(邏輯鏈路控制和適配協議)。 L2CAP 用作其他所有上層之間信息包數據的多路復用器。另一方面,RFCOMM 稱為“虛擬串行端口”層。 需要與支持數據流的設備通信時,RFCOMM 用起來不錯。OBEX(代表對象交換)是最適合文件傳輸的協議 層。借助 OBEX,可以創建消息並向包含有效載荷(也就是要發送的文件)的遠程藍牙設備發送消息以及 重要元數據(如文件名稱、文件大小和文件類型)。
藍牙模式
藍牙模式允許各種性能不同的藍牙設備進行交互和協作。每個模式都是一個針對具體目的定義功能的 用例。例如,如果希望通過移動設備向使用打印機,則兩台設備都必須實現基本打印模式。或者例如,如 果要同步台式機和 PDA 的聯系人列表,這兩台設備必須都支持同步模式。下面的表 1 列出使用藍牙棧 OBEX 協議層的模式。
表 1. 當前基於 OBEX 的模式
模式名稱 縮寫 UUID 對象推送模式 OPP 0x1105 文件傳輸模式 FTP 0x1106 同步模式 SYP 0x1104 靜態圖像傳輸模式 BIP 0x111A 電話簿訪問模式 PBAP 0x1130 基本打印模式 BPP 0x1122
根據藍牙 SIG,已定義的藍牙模式有 30 多種,涉及音頻分配到個人網。在本文中,我們將使用 JSR -82 API 實現對象推送模式,並向任意支持 OPP 的藍牙設備發送圖像。
創建 ImageSender Midlet
ImageSender Midlet 是使用 NetBeans 5.0 IDE 的 NetBeans Mobility Pack 創建的。Mobility Pack 包含一個非常方便的 GUI 設計工具,它允許移動開發人員使用拖放技術快速創建移動應用程序。 ImageSender Midlet 包含若干個靜態 GUI 組件(也就是非動態創建的組件),Mobility Pack 在創建 GUI 組件及其之間的工作流方面非常高效。以下圖 2 描述了用於創建 ImageSender Midlet 的 NetBeans 項目。
圖 2:用來創建 ImageSender Midlet 的 NetBeans 項目
為了使 ImageSender 完成其任務(如從文件系統讀取文件,以及向遠程藍牙設備發送數據), ImageSender 所使用的內部類封裝了以下三個重要功能領域:
讀取文件並遍歷文件系統(將由 FileNavigator 處理)
發現遠程藍牙設備(將由 BTUtility 處理)
使用對象推送模式向遠程藍牙設備發送文件(將由 FilePusher 處理)
學習了這些預備知識,下面開始實現!
ImageSender.FileNavigator
以下圖 3 是一個程序表,它顯示了 ImageSender Midlet 和其內部類(FileNavigator)之間的交互 ,FileNavigator 專門用來讀取和遍歷移動設備的文件系統。
圖 3:顯示 FileNavigator 內部類用法的程序圖
首先,ImageSender 獲取一個 FileNavigator 實例並調用 getListofFolder() 方法,該方法返回一 個 javax.microedition.lcdui.List。而 FileNavigator 將使用 JSR-75 File Connection API 的 FileSystemRegsitry 類獲取文件系統“根”的枚舉 ,也就是設備的載入點。如果移動設備包含可移動介 質(如 SD 內存卡),它也將在枚舉中顯示。對於文件系統的每個根,都對其建立一個 FileConnection 以確定它是文件還是文件夾。 這是必需的,因為您肯定希望以不同的方式處理它們(也就是,如果該項 目是文件夾,您希望遍歷該文件夾,但是如果該項目是文件,那麼您將希望打開該文件以獲得其內容)。 枚舉完之後,FileNavigator 內部類將向 ImageSender 返回一個 List,ImageSender 將簡單地在該移動 設備上顯示 List,如以下圖 4 所示。
圖 4:ImageSender 顯示目錄中的文件和文件夾列
因為 FileNavigator 內部類實現了 CommandListener 接口,所以它將處理來自該用戶接口的所有更 改目錄或選擇文件的請求。該方法使得父類 ImageSender 不再負責響應用戶的輸入並了解如何處理該輸 入。內部類已經擁有對 JSR-75 類的引用,這些類允許其連接到文件系統,所以非常適合處理用戶的請求 並處理文件系統。下面是 FileNavigator 的 commandAction() 方法的一部分;當用戶選擇該列表中的項 目時將執行這部分代碼:
...
if(isFolderSelected == true){
// the user selected a folder, so navigate down it
String folderUrl = (String)curr_dir_urls.elementAt (selectedIndex);
getDisplay().setCurrent(fileNavigator.getListofFolder(folderUrl, false));
} else {
getDisplay().setCurrent(get_fileSelectedAlert(), displayable);
// the user has obviously selected a file, so let's read it in
String file_url = (String)curr_dir_urls.elementAt (selectedIndex);
...
fileConn = (FileConnection) Connector.open(file_url);
InputStream is = fileConn.openInputStream();
// now let's read the file in into our byte[]
file = new byte[(int)fileConn.fileSize()];
is.read(file);
is.close();
...
可以看到,如果用戶選擇了一個文件夾,FileNavigator 將遍歷該文件夾,返回另一個列表並顯示它 。然後,如果該用戶選擇一個文件,FileNavigator 內部類將打開到該文件的 FileConnection 並以名為 “file”的字節數組讀取其內容。
ImageSender.BTUtility
ImageSender Midlet 使用的第二個輔助類是 BTUtility。您可能會猜到,BTUtility 內部類封裝了其 余代碼所調用的所有 JSR-82 藍牙 API 方法。BTUtility 主要向 ImageSender Midlet 提供兩個領域的 功能:發現附近的遠程藍牙設備,並對這些設備執行服務搜索。圖 5 為顯示 ImageSender 如何使用 BTUtility 的程序圖。
圖 5:描述 ImageSender 和 BTUtility 之間交互的程序圖
如上所示,ImageSender 獲得了 BTUtility 的新實例之後,該實例包含對 LocalDevice 和 DiscoveryAgent 類的引用。為了發現附近的藍牙設備,必須調用 DiscoveryAgent.startInquiry()。該 實現將異步調用在該區域發現的每台藍牙設備的 deviceDiscovered() 方法。最後,如果沒有發現更多藍 牙設備,JVM 將調用 inquiryCompleted() 方法。圖 6 顯示了 BTUtility 類發現的啟用藍牙技術的設備 列表。
圖 6:BTUtility 類所發現的藍牙設備
BTUtility 執行的另一項工作是搜索遠程藍牙設備上的服務。如以下圖 7 所示,搜索服務過程比發現 設備需要占用更多 CPU 時間。這就是為什麼設備發現過程可以從 BTUtility 的構造函數發起,但是服務 搜索部分必須以 Thread 啟動。
圖 7:使用 BTUtility 搜索遠程藍牙設備上的服務。
幸運的是,BTUtility 擴展了 Thread 類,因此可防止該用戶接口掛起。當 BTUtility 調用 DiscoveryAgent.searchServices() 時,如果發現匹配的服務,JVM 將異步調用其 serviceDiscovered() 方法。服務搜索過程完成時,JVM 將調用其 serviceSearchCompleted() 方法。作為備用方式,可以調用 DiscoveryAgent.selectService(),但是根據 JSR-82 規范,它只能返回附近一個服務提供商的 connectionURL。回顧上面的圖 6 可知,在大多數環境中,您不知道文件將發送給誰。下面是完整的 BTUtility 清單:
/**
* This is an inner class that is used for finding
* Bluetooth devices in the vicinity
*
*/
class BTUtility extends Thread implements DiscoveryListener {
Vector remoteDevices = new Vector();
Vector deviceNames = new Vector();
DiscoveryAgent discoveryAgent;
// obviously, 0x1105 is the UUID for
// the Object Push Profile
UUID[] uuidSet = {new UUID(0x1105) };
// 0x0100 is the attrubute for the service name element
// in the service record
int[] attrSet = {0x0100};
public void run(){
try {
RemoteDevice remoteDevice = (RemoteDevice)remoteDevices.elementAt (get_devicesList().getSelectedIndex());
discoveryAgent.searchServices(attrSet, uuidSet, remoteDevice , this);
} catch(Exception e) {
e.printStackTrace();
}
}
public BTUtility() {
// clear the list out, just in case it's not
get_devicesList().deleteAll();
try {
LocalDevice localDevice = LocalDevice.getLocalDevice();
discoveryAgent = localDevice.getDiscoveryAgent();
//deviceDiscoveryPanel.updateStatus(" Searching for Bluetooth devices in the vicinity...n");
discoveryAgent.startInquiry(DiscoveryAgent.GIAC, this);
} catch(Exception e) {
e.printStackTrace();
}
}
public void deviceDiscovered(RemoteDevice remoteDevice, DeviceClass cod) {
try{
remoteDevices.addElement(remoteDevice);
} catch(Exception e){
e.printStackTrace();
}
}
public void inquiryCompleted(int discType) {
if (remoteDevices.size() > 0) {
// the discovery process was a success
// so let's out them in a List and display it to the user
for (int i=0; i
在 BTUtility 內部類中,您可能會注意到在多處都使用了十六進制值。尤其是:
UUID[] uuidSet = {new UUID(0x1105) };
// 0x0100 is the attribute for the service name element
// in the service record
int[] attrSet = {0x0100};
現在,如果您記得上面的表 1 中的值,就可以理解為什麼創建 0x1105 UUID 值,因為它是對象推送 模式的 UUID。然而,在 attrSet 中也使用了 0x0100 值,通過它可以了解遠程服務的服務名稱。
ImageSender.FilePusher
到目前為止我們有哪些收獲?首先,我們擁有一個可以使用 JSR-75 FileConnection API 浏覽文件系 統的 Midlet(當然這都是內部類 FileNavigator 實現的)。Midlet 還能夠發現附近的遠程藍牙設備並 確定該設備上可用的服務(這項功能是由另一個內部類 BTUtility 提供的)。所以,現在只需實現使用 BBEX 向遠程藍牙設備發送文件的機制。
FilePusher 通過使用 org.netbeans.microedition.lcdui.WaitScreen(如圖 8 所示)並實現 org.netbeans.microedition.util.CancellableTask(它擴展了 Runnable)輕而易舉地完成了這項任務 。
圖 8:使用 WaitScreen 的 NetBeans 流設計快照
如上圖所示,WaitScreen 將執行 CPU 密集型任務(如執行網絡 I/O)。該任務必須實現 CancellableTask 接口(正是 FilePusher 內部類實現了該功能)。使用 IDE,可以用圖形的方式連接到 成功或失敗屏幕(具體取決於操作結果)。FilePusher 代碼如以下列表所示:
class FilePusher implements CancellableTask{
// this is used for the purposes of the Cancellable task
boolean isOperationFailed = false;
// this is the failure message used by the Cancellable task
String failure_message = null;
// this is the connection object to be used for
// bluetooth i/o
Connection connection = null;
public FilePusher(){
}
// this is method 1 of 4 methods needed to be implemented by
// the CancellableTask interface
public boolean cancel(){
// sorry this can't be cancelled
return false;
}
// this is method 2 of 4 needed to be implemented by
// the CancellableTask interface
public String getFailureMessage(){
// of course, if there's no problem
// then this method should return null as specified by the javadoc
return failure_message;
}
// this is method 3 of 4 needed to be implemented by
// the CancellableTask interface
public boolean hasFailed(){
return isOperationFailed;
}
// this is method 4 of 4 needed to be implemented by
// the CancellableTask interface
public void run(){
try{
connection = Connector.open(btConnectionURL);
// connection obtained
// now, let's create a session and a headerset objects
ClientSession cs = (ClientSession)connection;
HeaderSet hs = cs.createHeaderSet();
// now let's send the connect header
cs.connect(hs);
hs.setHeader(HeaderSet.NAME, filename);
hs.setHeader(HeaderSet.TYPE, "image/jpeg");
hs.setHeader(HeaderSet.LENGTH, new Long(file.length));
Operation putOperation = cs.put(hs);
OutputStream outputStream = putOperation.openOutputStream();
outputStream.write(file);
// file push complete
outputStream.close();
putOperation.close();
cs.disconnect(null);
connection.close();
} catch (Exception e){
isOperationFailed = true;
failure_message = e.toString();
}
}
}
現在,通過研究 ObjectPusher 內部類的 run() 方法來深入地了解 OBEX 協議。了解 OBEX 協議的最 佳方式是觀察 OBEX 客戶端和服務器之間的交互,如以下圖 9 所示:
圖 9:使用 OBEX 協議的客戶端和服務器交互示例
圖 9 中所示的場景發生在客戶端和服務器建立了物理藍牙連接的情況下,在 FilePusher 中,這是通 過以下代碼實現的:
connection = Connector.open(btConnectionURL);
// connection obtained
建立了連接之後,就應該通過將該連接對象更改成 ClientSession 對象來創建客戶端和服務器之間的 會話:
// now, let's create a session and a headerset object
ClientSession cs = (ClientSession)connection;
現在,建立會話之後,再看圖 9。請注意客戶端發送 OBEX 操作(如 Connect、Setpath、Get、Push 等),服務器將借助 OBEX Response Code(160、161、193、196等)響應客戶端。以下代碼展示如何發 送 Connect 操作:
// now let's send the connect operation
cs.connect(hs);
從以下代碼片斷可以看出,OBEX 是通過藍牙技術傳輸文件的首選方式(相比 RFCOMM 或 L2CAP),因 為可以使用報頭設置關於要傳輸文件的元數據:
hs.setHeader(HeaderSet.NAME, filename);
hs.setHeader(HeaderSet.TYPE, "image/jpeg");
hs.setHeader(HeaderSet.LENGTH, new Long(file.length));
這樣,接受者就能在接收該文件之前了解文件的名稱、文件類型和大小,而這是藍牙棧中其他協議層 不支持的強大功能。現在,設置完合適的報頭之後,將向服務器發送 Put 操作:
Operation putOperation = cs.put(hs);
該方法返回時,將得到一個 Operation 對象,您可以對其打開 OutputStream 並發送存儲在名為 “file”的字節數組中的文件數據(如果記得的話,可以知道我們是從 FileNavigator 內部類填充 “file”的)。現在如果想了解服務器的響應代碼,可以對 Operation 調用 getResponseCodes() 方法 。
因此,使用從 putOperation 打開的 OutputStream 向目的地發送文件之後,發送最後的 OBEX 操作 來完成程序:Disconnect Operation。
cs.disconnect(null);
結束語
可以看出,創建使用 JSR-82 和 JSR-75 API 進行無線圖像傳輸的復合應用程序並不需要太多工作。 盡管本文展示如何通過 OBEX 傳輸圖像,但是您會發現可以輕松修改這些代碼來發送文件類型。享受編碼 的樂趣吧!