詳見http://baike.baidu.com/view/758770.htm#5
目前,很多手機已經具備了藍牙功能。雖然MIDP2.0沒有包括藍牙API,但是JCP定義了JSR82, Java APIs for Bluetooth Wireless Technology (JABWT).這是一個可選API,很多支持MIDP2.0的手機已經實現了,比如Nokia 6600, Nokia 6670,Nokia7610等等。
目錄
簡介
Java 藍牙 API
堆棧初始化
設備管理
設備發現
服務發現
通訊
實例代碼
編輯本段
簡介
對於一個開發者來說,如果目標平臺支持JSR82的話,在製作聯網對戰類型遊戲或者應用的時候,藍牙是一個相當不錯的選擇。本文給出了一個最簡單的藍牙應用的J2ME程序,用以幫助開發者快速的掌握JSR82。該程序分別在2臺藍牙設備上安裝後,一臺設備作爲服務端先運行,一臺設備作爲客戶端後運行。在服務端上我們發佈了一個服務,該服務的功能是把客戶端發過來的字符串轉變爲大寫字符串。客戶端起動並搜索到服務端的服務後,我們就可以從客戶端的輸入框裏輸入任意的字符串,發送到服務端去,同時觀察服務端的反饋結果。
本文並不具體講述藍牙的運行機制和JSR82的API結構,關於這些知識點,請參考本文的參考資料一節,這些參考資料會給你一個權威的精確的解釋。
本文我們首先介紹在移動設備上進行java開發的基本原理,然後描述如何爲藍牙通訊編寫java應用。
編輯本段
Java 藍牙 API
Java藍牙 API依賴java通用連接框架,一直一來這成爲java 藍牙API應用的一個侷限。但是,人們建議將GCF加入到J2SE中。Java藍牙API使得訪問更多的系統成爲可能。
Java藍牙API定義了兩個包:一個是Java藍牙API的核心javax.bluetooth,另一個是用於對象交換協議的javax.obex(OBEX)。
根據JSR 82 規範,所有潛在藍牙系統都必須支持藍牙控制中心(BCC),該控制中心是一個控制面板,它的作用類似於可以讓用戶或OEM給堆棧中的某些配置參數定義具體值得應用程序,特別是,它將應用於堆棧初始化中。
任何藍牙應用都有以下這些組件:堆棧初始化組件,設備管理組件,設備發現組件,服務發現組件和通訊組件。
編輯本段
堆棧初始化
在開始無線通訊之前,你需要以銷售商預訂的方式初始化藍牙設備。(具體的堆棧初始化步驟超出了藍牙API規範的範圍。)
在一篇關於java與藍牙技術起步的java net文章中,Bruce Hopkins(java與藍牙技術的作者)向我們介紹了在Atinav java藍牙開發平臺上是如何通過一系列設置完成初始化工作的。(見列表A),在JSR 82規範不包含這些調用,這一點很重要,因爲其它的JSR82實現可能包括其它的初始化堆棧的方式。
編輯本段
設備管理
JSR82規範介紹了用於設備管理的兩個類:LocalDevice 和 RemoteDevice.
LocalDevice 允許你請求獲得藍牙設備的靜態信息。它依靠javax.bluetooth.DeviceClass類來獲得設備類型和它所提供的服務類型。
RemoteDevice可用來獲得藍牙鄰近區的設備信息(例如,某個遠程藍牙設備的地址)。它可以代表一臺遠程設備(例如,一臺在可到達範圍內的設備),並提供相應的方法來獲得關於這臺設備的有關信息,包括它的藍牙地址和名稱。
每個藍牙設備有一個唯一的硬件地址,像計算機的MAC地址一樣。你可以設定設備發現的級別,通過調用LocalDevice 對象中的setDiscoverable()方法可以使得其它藍牙設備發現當前設備。(見列表B)
編輯本段
設備發現
無線設備需要一種機制來允許它們發現其它的設備並訪問它們的功能。核心藍牙API的DiscoveryAgent 類和DiscoveryListener接口提供了需要的發現服務。有三種方式獲得可訪問設備列表。DiscoveryAgent.startInquiry()方法可將設備設置爲查詢模式,爲了充分利用這種模式,應用必須要指定一個事件監聽器來對與查詢相關的事件作出反應。當查詢完成或取消時,會調用DiscoveryListener.inquiryCompleted()方法。
如果一臺設備不想等待發現其它的設備,可以使用DiscoveryAgent.retrieveDevices()方法來獲得一個已經存在的列表。該方法或者返回一個在前面的查詢中發現的設備列表,或者返回一個預知的設備列表,這些設備是由本地設備提前告訴藍牙控制中心的它經常聯繫的設備。返回那種列表取決於傳遞的參數。列表C演示了最簡單的一種方式,當檢測到一臺新的藍牙設備時,對象需要使用DiscoveryAgent通過DiscoveryListener接口通知你。
編輯本段
服務發現
服務發現允許你發現附近的服務,而不管哪一臺設備提供的該服務。DiscoveryAgent提供的方法可以用來發現藍牙服務設備上的服務,並初始化服務發現事務。在服務可以被發現以前,必須首先在藍牙服務設備上註冊或廣播該服務。服務設備負責完成很多任務,包括創建描述所提供的服務的服務記錄,接受來自客戶端的連接,向服務設備的服務發現數據庫(SDDB)添加新的服務記錄。總之,它的工作類似於web服務器。列表D是服務註冊的一個例子。
編輯本段
通訊
兩臺設備必須共享通用的通訊協議才能通訊。爲了應用能夠訪問更多的藍牙服務,藍牙java API提供了這樣一個機制,它允許連接到使用RFCOMM, L2CAP, 或 OBEX協議的任何服務。如果服務使用了位於上面協議之上其它的協議(例如TCP/IP),只有在應用中利用CLDC通用連接框架實現額外的協議,纔可以訪問該服務。
用於服務記錄的URL包括數字和符號,大體是這樣的結構:
btspp://508031205080110F1B1B1D1C100:8.它的意思是客戶應該使用藍牙串口框架來建立到地址爲508031205080110F1B1B1D1C100的設備的8號服務。設備地址和計算機的物理地址相似,列表E顯示了簡單的RFCOMM連接。
Peter V. Mikhalenko是sun公司認證的專業IT人員,是Deutsche銀行的業務顧問。
編輯本段
實例代碼
該程序包括3個java文件。一個是MIDlet,另外2個爲服務端GUI和客戶端GUI。該程序已經在wtk22模擬器和Nokia 6600,Nokia 6670兩款手機上測試通過。
StupidBTMIDlet.java
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.List;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
/**
* @author Jagie
*
* MIDlet
*/
public class StupidBTMIDlet extends MIDlet implements CommandListener {
List list;
ServerBox sb;
ClientBox cb;
/*
* (non-Javadoc)
*
* @see javax.microedition.midlet.MIDlet#startApp()
*/
protected void startApp() throws MIDletStateChangeException {
list = new List("傻瓜藍牙入門", List.IMPLICIT);
list.append("Client", null);
list.append("Server", null);
list.setCommandListener(this);
Display.getDisplay(this).setCurrent(list);
}
/**
* debug方法
* @param s 要顯示的字串
*/
public void showString(String s) {
Displayable dp = Display.getDisplay(this).getCurrent();
Alert al = new Alert(null, s, null, AlertType.INFO);
al.setTimeout(2000);
Display.getDisplay(this).setCurrent(al, dp);
}
/**
* 顯示主菜單
*
*/
public void showMainMenu() {
Display.getDisplay(this).setCurrent(list);
}
protected void pauseApp() {
// TODO Auto-generated method stub
}
public void commandAction(Command com, Displayable disp) {
if (com == List.SELECT_COMMAND) {
List list = (List) disp;
int index = list.getSelectedIndex();
if (index == 1) {
if (sb == null) {
sb = new ServerBox(this);
}
sb.setString(null);
Display.getDisplay(this).setCurrent(sb);
} else {
//每次都生成新的客戶端實例
cb = null;
System.gc();
cb = new ClientBox(this);
Display.getDisplay(this).setCurrent(cb);
}
}
}
protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
// TODO Auto-generated method stub
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
ServerBox.java
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Vector;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.TextBox;
import javax.microedition.lcdui.TextField;
/**
* 服務端GUI
* @author Jagie
*
* TODO To change the template for this generated type comment go to
* Window - Preferences - Java - Code Style - Code Templates
*/
public class ServerBox extends TextBox implements Runnable, CommandListener {
Command com_pub = new Command("開啓服務", Command.OK, 0);
Command com_cancel = new Command("終止服務", Command.CANCEL, 0);
Command com_back = new Command("返回", Command.BACK, 1);
LocalDevice localDevice;
StreamConnectionNotifier notifier;
ServiceRecord record;
boolean isClosed;
ClientProcessor processor;
StupidBTMIDlet midlet;
//響應服務的uuid
private static final UUID ECHO_SERVER_UUID = new UUID(
"F0E0D0C0B0A000908070605040302010", false);
public ServerBox(StupidBTMIDlet midlet) {
super(null, "", 500, TextField.ANY);
this.midlet = midlet;
this.addCommand(com_pub);
this.addCommand(com_back);
this.setCommandListener(this);
}
public void run() {
boolean isBTReady = false;
try {
localDevice = LocalDevice.getLocalDevice();
if (!localDevice.setDiscoverable(DiscoveryAgent.GIAC)) {
showInfo("無法設置設備發現模式");
return;
}
// prepare a URL to create a notifier
StringBuffer url = new StringBuffer("btspp://");
// indicate this is a server
url.append("localhost").append(':');
// add the UUID to identify this service
url.append(ECHO_SERVER_UUID.toString());
// add the name for our service
url.append(";name=Echo Server");
// request all of the client not to be authorized
// some devices fail on authorize=true
url.append(";authorize=false");
// create notifier now
notifier = (StreamConnectionNotifier) Connector
.open(url.toString());
record = localDevice.getRecord(notifier);
// remember we've reached this point.
isBTReady = true;
} catch (Exception e) {
e.printStackTrace();
}
// nothing to do if no bluetooth available
if (isBTReady) {
showInfo("初始化成功,等待連接");
this.removeCommand(com_pub);
this.addCommand(com_cancel);
} else {
showInfo("初始化失敗,退出");
return;
}
// 生成服務端服務線程對象
processor = new ClientProcessor();
// ok, start accepting connections then
while (!isClosed) {
StreamConnection conn = null;
try {
conn = notifier.acceptAndOpen();
} catch (IOException e) {
// wrong client or interrupted - continue anyway
continue;
}
processor.addConnection(conn);
}
}
public void publish() {
isClosed = false;
this.setString(null);
new Thread(this).start();
}
public void cancelService() {
isClosed = true;
showInfo("服務終止");
this.removeCommand(com_cancel);
this.addCommand(com_pub);
}
/*
* (non-Javadoc)
*
* @see javax.microedition.lcdui.CommandListener#commandAction(javax.microedition.lcdui.Command,
* javax.microedition.lcdui.Displayable)
*/
public void commandAction(Command arg0, Displayable arg1) {
if (arg0 == com_pub) {
//發佈service
publish();
} else if (arg0 == com_cancel) {
cancelService();
} else {
cancelService();
midlet.showMainMenu();
}
}
/**
* 內部類,服務端服務線程。
* @author Jagie
*
* TODO To change the template for this generated type comment go to
* Window - Preferences - Java - Code Style - Code Templates
*/
private class ClientProcessor implements Runnable {
private Thread processorThread;
private Vector queue = new Vector();
private boolean isOk = true;
ClientProcessor() {
processorThread = new Thread(this);
processorThread.start();
}
public void run() {
while (!isClosed) {
synchronized (this) {
if (queue.size() == 0) {
try {
//阻塞,直到有新客戶連接
wait();
} catch (InterruptedException e) {
}
}
}
//處理連接隊列
StreamConnection conn;
synchronized (this) {
if (isClosed) {
return;
}
conn = (StreamConnection) queue.firstElement();
queue.removeElementAt(0);
processConnection(conn);
}
}
}
/**
* 往連接隊列添加新連接,同時喚醒處理線程
* @param conn
*/
void addConnection(StreamConnection conn) {
synchronized (this) {
queue.addElement(conn);
notify();
}
}
}
/**
* 從StreamConnection讀取輸入
* @param conn
* @return
*/
private String readInputString(StreamConnection conn) {
String inputString = null;
try {
DataInputStream dis = conn.openDataInputStream();
inputString = dis.readUTF();
dis.close();
} catch (Exception e) {
e.printStackTrace();
}
return inputString;
}
/**
* debug
* @param s
*/
private void showInfo(String s) {
StringBuffer sb = new StringBuffer(this.getString());
if (sb.length() > 0) {
sb.append("/n");
}
sb.append(s);
this.setString(sb.toString());
}
/**
* 處理客戶端連接
* @param conn
*/
private void processConnection(StreamConnection conn) {
// 讀取輸入
String inputString = readInputString(conn);
//生成響應
String outputString = inputString.toUpperCase();
//輸出響應
sendOutputData(outputString, conn);
try {
conn.close();
} catch (IOException e) {
} // ignore
showInfo("客戶端輸入:" + inputString + ",已成功響應!");
}
/**
* 輸出響應
* @param outputData
* @param conn
*/
private void sendOutputData(String outputData, StreamConnection conn) {
try {
DataOutputStream dos = conn.openDataOutputStream();
dos.writeUTF(outputData);
dos.close();
} catch (IOException e) {
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
ClientBox.java
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Vector;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Gauge;
import javax.microedition.lcdui.StringItem;
import javax.microedition.lcdui.TextField;
//jsr082 API
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
/**
* 客戶端GUI * @author Jagie *
* TODO To change the template for this generated type comment go to
* Window - Preferences - Java - Code Style - Code Templates
*/
public class ClientBox extends Form implements Runnable, CommandListener, DiscoveryListener {
//字串輸入框
TextField input = new TextField(null, "", 50, TextField.ANY);
//loger
StringItem result = new StringItem("結果:", "");
private DiscoveryAgent discoveryAgent;
private UUID[] uuidSet;
//響應服務的UUID
private static final UUID ECHO_SERVER_UUID = new UUID( "F0E0D0C0B0A000908070605040302010", false);
//設備集合
Vector devices = new Vector();
//服務集合
Vector records = new Vector();
//服務搜索的事務id集合
int[] transIDs;
StupidBTMIDlet midlet;
public ClientBox(StupidBTMIDlet midlet) {
super("");
this.midlet=midlet;
this.append(result);
this.addCommand(new Command("取消",Command.CANCEL,1));
this.setCommandListener(this);
new Thread(this).start();
}
public void commandAction(Command arg0, Displayable arg1) {
if(arg0.getCommandType()==Command.CANCEL){
midlet.showMainMenu();
}else{
//匿名內部Thread,訪問遠程服務。
Thread fetchThread=new Thread(){
public void run(){
for(int i=0;i<records.size();i++){
ServiceRecord sr=(ServiceRecord)records.elementAt(i);
if(accessService(sr)){ //訪問到一個可用的服務即可
break;
}
}
}
};
fetchThread.start();
}
}
private boolean accessService(ServiceRecord sr){
boolean result=false;
try {
String url = sr.getConnectionURL(
ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
StreamConnection conn = (StreamConnection) Connector.open(url);
DataOutputStream dos=conn.openDataOutputStream();
dos.writeUTF(input.getString());
dos.close();
DataInputStream dis=conn.openDataInputStream();
String echo=dis.readUTF();
dis.close();
showInfo("反饋結果是:"+echo);
result=true;
} catch (IOException e) {
}
return result;
}
public synchronized void run() {
//發現設備和服務的過程中,給用戶以Gauge
Gauge g=new Gauge(null,false,Gauge.INDEFINITE,Gauge.CONTINUOUS_RUNNING);
this.append(g);
showInfo("藍牙初始化...");
boolean isBTReady = false;
try {
LocalDevice localDevice = LocalDevice.getLocalDevice();
discoveryAgent = localDevice.getDiscoveryAgent();
isBTReady = true;
} catch (Exception e) {
e.printStackTrace();
}
if (!isBTReady) {
showInfo("藍牙不可用");
//刪除Gauge
this.delete(1);
return;
} uuidSet = new UUID[2];
//標誌我們的響應服務的UUID集合
uuidSet[0] = new UUID(0x1101);
uuidSet[1] = ECHO_SERVER_UUID;
try {
discoveryAgent.startInquiry(DiscoveryAgent.GIAC, this);
} catch (BluetoothStateException e) {
}
try {
//阻塞,由inquiryCompleted()回調方法喚醒
wait();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
showInfo("設備搜索完畢,共找到"+devices.size()+"個設備,開始搜索服務");
transIDs = new int[devices.size()];
for (int i = 0; i < devices.size(); i++) {
RemoteDevice rd = (RemoteDevice) devices.elementAt(i);
try {
//記錄每一次服務搜索的事務id
transIDs = discoveryAgent.searchServices(null, uuidSet,
rd, this);
} catch (BluetoothStateException e) {
continue;
}
}
try {
//阻塞,由serviceSearchCompleted()回調方法在所有設備都搜索完的情況下喚醒
wait();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
showInfo("服務搜索完畢,共找到"+records.size()+"個服務,準備發送請求");
if(records.size()>0){
this.append(input);
this.addCommand(new Command("發送",Command.OK,0));
}
//刪除Gauge
this.delete(1);
}
/**
* debug
* @param s
*/
private void showInfo(String s){
StringBuffer sb=new StringBuffer(result.getText());
if(sb.length()>0){
sb.append("/n");
}
sb.append(s);
result.setText(sb.toString());
}
/** * 回調方法 */
public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) {
if (devices.indexOf(btDevice) == -1) {
devices.addElement(btDevice);
}
}
/** * 回調方法,喚醒初始化線程 */
public void inquiryCompleted(int discType) {
synchronized (this) {
notify();
}
}
/** * 回調方法 */
public void servicesDiscovered(int transID, ServiceRecord[] servRecord) {
for (int i = 0; i < servRecord.length; i++) {
records.addElement(servRecord);
}
}
/** * 回調方法,喚醒初始化線程 */
public void serviceSearchCompleted(int transID, int respCode) {
for (int i = 0; i < transIDs.length; i++) {
if (transIDs == transID) {
transIDs = -1;
break;
}
}
//如果所有的設備都已經搜索服務完畢,則喚醒初始化線程。
boolean finished = true;
for (int i = 0; i < transIDs.length; i++) {
if (transIDs != -1) {
finished = false;
break;
}
}
if (finished) {
synchronized (this) {
notify();
}
}
}
}
另一個不錯的教程:
http://hi.baidu.com/jacobi198711 ... 30c62c6059f399.html
/**
LocalDevice類標識了本地藍牙設備。藍牙應用程序和LocalDevice之間的關係是典型的一對一關係:
本地設備提供了方法來返回關於本地設備的信息,並且能夠進入Bluetooth manager:
.getBluetoothAddress()返回藍牙設備地址。
.getDeviceClass()返回設備類。
.getFriendlyName()返回設備友好名稱,藍牙設備名通常是用戶在藍牙控制中心爲其設置的。
.getRecord()返回一個指定藍牙連接的服務記錄。
.updateRecord()方法用來爲指定的ServiceRecord更新SDDB服務記錄。
.getDiscoverable()返回設備的可發現狀態。
.setDiscoverable()設置設備的可發現狀態。
.getDiscoveryAgent()返回一個參考給發現代理。
.getProperty()返回一個設備的藍牙屬性
通過調用getProperty()方法你可以得到的屬性包括:
.bluetooth.api.version,藍牙API版本
.bluetooth.sd.attr.retrievable.max,一次性能夠被獲得的服務記錄屬性的最大值
.bluetooth.connected.devices.max,支持的連接設備的最大值
.bluetooth.sd.trans.max,同時發生的服務發現處理的最大值
.bluetooth.l2cap.receiveMTU.max,L2CAP最大發射單元
*/
/**
設備發現: discoveryAgent = localDevice.getDiscoveryAgent();
使用DiscoveryAgent類的"設備發現"方法來開始和取消設備發現:
.retrieveDevices()重新獲得已經發現或者附近的已知設備
.startInquiry() 啓動發現附近設備,也叫inquiry
.cancelInquiry()取消當前進行的任何請求
藍牙發現代理在請求階段的不同時候會分別調用DiscoveryListener(發現監聽器)不同的回調方法:
.deviceDiscovered() 指出是否有設備被發現。
.inquiryCompleted() 指出是否請求已經成功、觸發一個錯誤或已被取消。
設備發現以調用startInquiry()函數開始。
在請求進行時,藍牙發現代理會在適當的時候調用回調方法DeviceDiscovered()和inquiryCompleted()。
服務發現:
可以使用發現代理的服務發現方法來開始或取消服務發現:
.selectService()啓動服務發現搜索。(原文有誤,根據API手冊應爲嘗試定位一個服務)
.searchServices()啓動服務發現搜索。
.cancelServiceSearch()取消在正在進行中的任何的服務發現搜索操作。
藍牙發現代理在服務發現階段的不同時候會分別調用DiscoveryListener的服務發現回調方法:
.servicesDiscovered() 表示是否服務已被發現。
.serviceSearchCompleted()表示服務發現是否已經完成。
服務發現的狀態改變結束於DiscoveryListener的回調方法的返回。
服務發現開始於對searchServices()的調用。當服務搜索進行時,
藍牙發現代理會在適當的時候回調servicesDiscovered()和 serviceSearchCompleted()方法。
*/
/**
一個RemoteDevice的實例代表了一個遠端藍牙設備。
在一個藍牙客戶端應用程序可以進行服務,消費之前,它必須發送一個設備請求來發現遠端設備。
典型的藍牙應用程序和遠端設備之間的關係是一對多:
遠端設備(RemoteDevice)提供的方法中,有些很類似於本地設備(LocalDevice)裏提供的方法:
.getBluetoothAddress()返回藍牙地址。
.getFriendlyName()返回藍牙設備名。
.getRemoteDevice()返回相應的被指定藍牙連接的遠端設備。
.authenticate()嘗試識別驗證遠端設備。
.authorize()爲指定的藍牙連接去嘗試批准遠端設備訪問本地設備。
.encrypt()嘗試爲指定的藍牙連接開啓或關閉加密。
.isAuthenticated()測試是否遠端設備可以被驗證。
.isAuthorized()測試是否遠端設備已經被藍牙控制中心授權訪問本地設備以進行藍牙連接。
.isEncrypted()測試是否本地設備和遠端設備之間的通信被加密。
.isTrustedDevice()測試是否遠端設備被藍牙控制中心指定爲可信任的。
*/
j2me藍牙教程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.