J2ME蓝牙(bluetooth)实战入门

概述

目前,很多手机已经具备了蓝牙功能。虽然MIDP2.0没有包括蓝牙API,但是JCP定义了JSR82, Java APIs for Bluetooth Wireless Technology (JABWT).这是一个可选API,很多支持MIDP2.0的手机已经实现了,比如Nokia 6600, Nokia 6670,Nokia7610等等。对于一个开发者来说,如果目标平台支持JSR82的话,在制作联网对战类型游戏或者应用的时候,蓝牙是一个相当不错的选择。本文给出了一个最简单的蓝牙应用的J2ME程序,用以帮助开发者快速的掌握JSR82。该程序分别在2台蓝牙设备上安装后,一台设备作为服务端先运行,一台设备作为客户端后运行。在服务端上我们发布了一个服务,该服务的功能是把客户端发过来的字符串转变为大写字符串。客户端起动并搜索到服务端的服务后,我们就可以从客户端的输入框里输入任意的字符串,发送到服务端去,同时观察服务端的反馈结果。
    本文并不具体讲述蓝牙的运行机制和JSR82的API结构,关于这些知识点,请参考本文的参考资料一节,这些参考资料会给你一个权威的精确的解释。

实例代码

该程序包括3个java文件。一个是MIDlet,另外2个为服务端GUI和客户端GUI。该程序已经在wtk22模拟器和Nokia 6600,Nokia 6670两款手机上测试通过。

StupidBTMIDlet.java

  1. import javax.microedition.lcdui.Alert;
  2. import javax.microedition.lcdui.AlertType;
  3. import javax.microedition.lcdui.Command;
  4. import javax.microedition.lcdui.CommandListener;
  5. import javax.microedition.lcdui.Display;
  6. import javax.microedition.lcdui.Displayable;
  7. import javax.microedition.lcdui.List;
  8. import javax.microedition.midlet.MIDlet;
  9. import javax.microedition.midlet.MIDletStateChangeException;
  10. /**
  11.  * @author Jagie
  12.  * 
  13.  *  MIDlet
  14.  */
  15. public class StupidBTMIDlet extends MIDlet implements CommandListener {
  16.     List list;
  17.     ServerBox sb;
  18.     ClientBox cb;
  19.     /*
  20.      * (non-Javadoc)
  21.      * 
  22.      * @see javax.microedition.midlet.MIDlet#startApp()
  23.      */
  24.     protected void startApp() throws MIDletStateChangeException {
  25.         list = new List("傻瓜蓝牙入门"List.IMPLICIT);
  26.         list.append("Client"null);
  27.         list.append("Server"null);
  28.         list.setCommandListener(this);
  29.         Display.getDisplay(this).setCurrent(list);
  30.     }
  31.     
  32.     /**
  33.      * debug方法
  34.      * @param s 要显示的字串
  35.      */
  36.     public void showString(java/lang/String.java.html" target="_blank">String s) {
  37.         Displayable dp = Display.getDisplay(this).getCurrent();
  38.         Alert al = new Alert(null, s, null, AlertType.INFO);
  39.         al.setTimeout(2000);
  40.         Display.getDisplay(this).setCurrent(al, dp);
  41.     }
  42.     
  43.     /**
  44.      * 显示主菜单
  45.      *
  46.      */
  47.     public void showMainMenu() {
  48.         Display.getDisplay(this).setCurrent(list);
  49.     }
  50.     
  51.     protected void pauseApp() {
  52.         // TODO Auto-generated method stub
  53.     }
  54.     public void commandAction(Command com, Displayable disp) {
  55.         if (com == List.SELECT_COMMAND) {
  56.             List list = (List) disp;
  57.             int index = list.getSelectedIndex();
  58.             if (index == 1) {
  59.                 if (sb == null) {
  60.                     sb = new ServerBox(this);
  61.                 }
  62.                 sb.setString(null);
  63.                 Display.getDisplay(this).setCurrent(sb);
  64.             } else {
  65.                 //每次都生成新的客户端实例
  66.                 cb = null;
  67.                 java/lang/System.java.html" target="_blank">System.gc();
  68.                 cb = new ClientBox(this);
  69.                 Display.getDisplay(this).setCurrent(cb);
  70.             }
  71.         }
  72.     }
  73.     protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
  74.         // TODO Auto-generated method stub
  75.     }
  76. }

ClientBox.java

  1. import java.io.java/io/DataInputStream.java.html" target="_blank">DataInputStream;
  2. import java.io.java/io/DataOutputStream.java.html" target="_blank">DataOutputStream;
  3. import java.io.java/io/IOException.java.html" target="_blank">IOException;
  4. import java.util.java/util/Vector.java.html" target="_blank">Vector;
  5. import javax.microedition.io.Connector;
  6. import javax.microedition.io.StreamConnection;
  7. import javax.microedition.lcdui.Command;
  8. import javax.microedition.lcdui.CommandListener;
  9. import javax.microedition.lcdui.Displayable;
  10. import javax.microedition.lcdui.Form;
  11. import javax.microedition.lcdui.Gauge;
  12. import javax.microedition.lcdui.StringItem;
  13. import javax.microedition.lcdui.TextField;
  14. //jsr082 API
  15. import javax.bluetooth.BluetoothStateException;
  16. import javax.bluetooth.DeviceClass;
  17. import javax.bluetooth.DiscoveryAgent;
  18. import javax.bluetooth.DiscoveryListener;
  19. import javax.bluetooth.LocalDevice;
  20. import javax.bluetooth.RemoteDevice;
  21. import javax.bluetooth.ServiceRecord;
  22. import javax.bluetooth.UUID;
  23. /**
  24.  * 客户端GUI
  25.  * @author Jagie
  26.  *
  27.  * TODO To change the template for this generated type comment go to
  28.  * Window – Preferences – Java – Code Style – Code Templates
  29.  */
  30. public class ClientBox extends Form implements java/lang/Runnable.java.html" target="_blank">Runnable, CommandListener,
  31.         DiscoveryListener {
  32.     
  33.     //字串输入框
  34.     TextField input = new TextField(null"", 50, TextField.ANY);
  35.     //loger
  36.     StringItem result = new StringItem("结果:""");
  37.     private DiscoveryAgent discoveryAgent;
  38.     
  39.     private UUID[] uuidSet;
  40.     //响应服务的UUID
  41.     private static final UUID ECHO_SERVER_UUID = new UUID(
  42.             "F0E0D0C0B0A000908070605040302010"false);
  43.     //设备集合
  44.     java/util/Vector.java.html" target="_blank">Vector devices = new java/util/Vector.java.html" target="_blank">Vector();
  45.     //服务集合
  46.     java/util/Vector.java.html" target="_blank">Vector records = new java/util/Vector.java.html" target="_blank">Vector();
  47.     
  48.     //服务搜索的事务id集合
  49.     int[] transIDs;
  50.     StupidBTMIDlet midlet;
  51.     public ClientBox(StupidBTMIDlet midlet) {
  52.         super("");
  53.         this.midlet=midlet;
  54.         
  55.         this.append(result);
  56.         
  57.         this.addCommand(new Command("取消",Command.CANCEL,1));
  58.         this.setCommandListener(this);
  59.         
  60.         new java/lang/Thread.java.html" target="_blank">Thread(this).start();
  61.     }
  62.     
  63.     public void commandAction(Command arg0, Displayable arg1) {
  64.         if(arg0.getCommandType()==Command.CANCEL){
  65.             midlet.showMainMenu();
  66.         }else{
  67.             //匿名内部Thread,访问远程服务。
  68.             java/lang/Thread.java.html" target="_blank">Thread fetchThread=new java/lang/Thread.java.html" target="_blank">Thread(){
  69.                 public void run(){
  70.                     for(int i=0;i<records.size();i++){
  71.                         ServiceRecord sr=(ServiceRecord)records.elementAt(i);
  72.                         if(accessService(sr)){
  73.                             //访问到一个可用的服务即可
  74.                             break;
  75.                         }
  76.                     }
  77.                 }
  78.             };
  79.             fetchThread.start();
  80.         }
  81.         
  82.     }
  83.     
  84.     
  85.     private boolean  accessService(ServiceRecord sr){
  86.         boolean result=false;
  87.          try {
  88.             java/lang/String.java.html" target="_blank">String url = sr.getConnectionURL(
  89.                     ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
  90.             StreamConnection    conn = (StreamConnection) Connector.open(url);
  91.             
  92.             java/io/DataOutputStream.java.html" target="_blank">DataOutputStream dos=conn.openDataOutputStream();
  93.             dos.writeUTF(input.getString());
  94.             dos.close();
  95.             java/io/DataInputStream.java.html" target="_blank">DataInputStream dis=conn.openDataInputStream();
  96.             java/lang/String.java.html" target="_blank">String echo=dis.readUTF();
  97.             dis.close();
  98.             showInfo("反馈结果是:"+echo);
  99.             result=true;
  100.             
  101.         } catch (java/io/IOException.java.html" target="_blank">IOException e) {
  102.             
  103.         }
  104.         return result;
  105.     }
  106.     public synchronized void run() {
  107.         //发现设备和服务的过程中,给用户以Gauge
  108.         Gauge g=new Gauge(null,false,Gauge.INDEFINITE,Gauge.CONTINUOUS_RUNNING);
  109.         this.append(g);
  110.         showInfo("蓝牙初始化…");
  111.         boolean isBTReady = false;
  112.         try {
  113.             LocalDevice localDevice = LocalDevice.getLocalDevice();
  114.             discoveryAgent = localDevice.getDiscoveryAgent();
  115.             isBTReady = true;
  116.         } catch (java/lang/Exception.java.html" target="_blank">Exception e) {
  117.             e.printStackTrace();
  118.         }
  119.         if (!isBTReady) {
  120.             showInfo("蓝牙不可用");
  121.             //删除Gauge
  122.             this.delete(1);
  123.             return;
  124.         }
  125.         uuidSet = new UUID[2];
  126.         //标志我们的响应服务的UUID集合
  127.         uuidSet[0] = new UUID(0x1101);
  128.         uuidSet[1] = ECHO_SERVER_UUID;
  129.         
  130.         try {
  131.             discoveryAgent.startInquiry(DiscoveryAgent.GIAC, this);
  132.         } catch (BluetoothStateException e) {
  133.         }
  134.         try {
  135.             //阻塞,由inquiryCompleted()回调方法唤醒
  136.             wait();
  137.         } catch (java/lang/InterruptedException.java.html" target="_blank">InterruptedException e1) {
  138.             
  139.             e1.printStackTrace();
  140.         }
  141.         showInfo("设备搜索完毕,共找到"+devices.size()+"个设备,开始搜索服务");
  142.         transIDs = new int[devices.size()];
  143.         for (int i = 0; i < devices.size(); i++) {
  144.             RemoteDevice rd = (RemoteDevice) devices.elementAt(i);
  145.             try {
  146.                 //记录每一次服务搜索的事务id
  147.                 transIDs[i] = discoveryAgent.searchServices(null, uuidSet,
  148.                         rd, this);
  149.             } catch (BluetoothStateException e) {
  150.                 continue;
  151.             }
  152.         }
  153.         
  154.         try {
  155.             //阻塞,由serviceSearchCompleted()回调方法在所有设备都搜索完的情况下唤醒
  156.             wait();
  157.         } catch (java/lang/InterruptedException.java.html" target="_blank">InterruptedException e1) {
  158.             e1.printStackTrace();
  159.         }
  160.         
  161.         showInfo("服务搜索完毕,共找到"+records.size()+"个服务,准备发送请求");
  162.         if(records.size()>0){
  163.             this.append(input);
  164.             this.addCommand(new Command("发送",Command.OK,0));
  165.         }
  166.         
  167.         //删除Gauge
  168.         this.delete(1);
  169.         
  170.     }
  171.     
  172.     /**
  173.      * debug
  174.      * @param s
  175.      */
  176.     
  177.     private void showInfo(java/lang/String.java.html" target="_blank">String s){
  178.         java/lang/StringBuffer.java.html" target="_blank">StringBuffer sb=new java/lang/StringBuffer.java.html" target="_blank">StringBuffer(result.getText());
  179.         if(sb.length()>0){
  180.             sb.append("\n");
  181.         }
  182.         sb.append(s);
  183.         result.setText(sb.toString());
  184.     }
  185.     
  186.     /**
  187.      * 回调方法
  188.      */
  189.     public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) {
  190.         if (devices.indexOf(btDevice) == -1) {
  191.             devices.addElement(btDevice);
  192.         }
  193.     }
  194.     /**
  195.      * 回调方法,唤醒初始化线程
  196.      */
  197.     public void inquiryCompleted(int discType) {
  198.         synchronized (this) {
  199.             notify();
  200.         }
  201.     }
  202.     /**
  203.      * 回调方法
  204.      */
  205.     public void servicesDiscovered(int transID, ServiceRecord[] servRecord) {
  206.         for (int i = 0; i < servRecord.length; i++) {
  207.             records.addElement(servRecord[i]);
  208.         }
  209.     }
  210.     
  211.     /**
  212.      * 回调方法,唤醒初始化线程
  213.      */
  214.     public void serviceSearchCompleted(int transID, int respCode) {
  215.         
  216.         for (int i = 0; i < transIDs.length; i++) {
  217.             if (transIDs[i] == transID) {
  218.                 transIDs[i] = -1;
  219.                 break;
  220.             }
  221.         }
  222.         
  223.         //如果所有的设备都已经搜索服务完毕,则唤醒初始化线程。
  224.         boolean finished = true;
  225.         for (int i = 0; i < transIDs.length; i++) {
  226.             if (transIDs[i] != -1) {
  227.                 finished = false;
  228.                 break;
  229.             }
  230.         }
  231.         if (finished) {
  232.             synchronized (this) {
  233.                 notify();
  234.             }
  235.         }
  236.     }
  237. }

ServerBox.java

  1. import java.io.java/io/DataInputStream.java.html" target="_blank">DataInputStream;
  2. import java.io.java/io/DataOutputStream.java.html" target="_blank">DataOutputStream;
  3. import java.io.java/io/IOException.java.html" target="_blank">IOException;
  4. import java.util.java/util/Vector.java.html" target="_blank">Vector;
  5. import javax.bluetooth.DiscoveryAgent;
  6. import javax.bluetooth.LocalDevice;
  7. import javax.bluetooth.ServiceRecord;
  8. import javax.bluetooth.UUID;
  9. import javax.microedition.io.Connector;
  10. import javax.microedition.io.StreamConnection;
  11. import javax.microedition.io.StreamConnectionNotifier;
  12. import javax.microedition.lcdui.Command;
  13. import javax.microedition.lcdui.CommandListener;
  14. import javax.microedition.lcdui.Displayable;
  15. import javax.microedition.lcdui.TextBox;
  16. import javax.microedition.lcdui.TextField;
  17. /**
  18.  * 服务端GUI
  19.  * @author Jagie
  20.  *
  21.  * TODO To change the template for this generated type comment go to
  22.  * Window – Preferences – Java – Code Style – Code Templates
  23.  */
  24. public class ServerBox extends TextBox implements java/lang/Runnable.java.html" target="_blank">Runnable, CommandListener {
  25.     Command com_pub = new Command("开启服务", Command.OK, 0);
  26.     Command com_cancel = new Command("终止服务", Command.CANCEL, 0);
  27.     Command com_back = new Command("返回", Command.BACK, 1);
  28.     LocalDevice localDevice;
  29.     StreamConnectionNotifier notifier;
  30.     ServiceRecord record;
  31.     boolean isClosed;
  32.     ClientProcessor processor;
  33.     StupidBTMIDlet midlet;
  34.     //响应服务的uuid
  35.     private static final UUID ECHO_SERVER_UUID = new UUID(
  36.             "F0E0D0C0B0A000908070605040302010"false);
  37.     public ServerBox(StupidBTMIDlet midlet) {
  38.         super(null"", 500, TextField.ANY);
  39.         this.midlet = midlet;
  40.         this.addCommand(com_pub);
  41.         this.addCommand(com_back);
  42.         this.setCommandListener(this);
  43.     }
  44.     public void run() {
  45.         boolean isBTReady = false;
  46.         try {
  47.             localDevice = LocalDevice.getLocalDevice();
  48.             if (!localDevice.setDiscoverable(DiscoveryAgent.GIAC)) {
  49.                 showInfo("无法设置设备发现模式");
  50.                 return;
  51.             }
  52.             // prepare a URL to create a notifier
  53.             java/lang/StringBuffer.java.html" target="_blank">StringBuffer url = new java/lang/StringBuffer.java.html" target="_blank">StringBuffer("btspp://");
  54.             // indicate this is a server
  55.             url.append("localhost").append(:);
  56.             // add the UUID to identify this service
  57.             url.append(ECHO_SERVER_UUID.toString());
  58.             // add the name for our service
  59.             url.append(";name=Echo Server");
  60.             // request all of the client not to be authorized
  61.             // some devices fail on authorize=true
  62.             url.append(";authorize=false");
  63.             // create notifier now
  64.             notifier = (StreamConnectionNotifier) Connector
  65.                     .open(url.toString());
  66.             record = localDevice.getRecord(notifier);
  67.             // remember weve reached this point.
  68.             isBTReady = true;
  69.         } catch (java/lang/Exception.java.html" target="_blank">Exception e) {
  70.             e.printStackTrace();
  71.             
  72.         }
  73.         // nothing to do if no bluetooth available
  74.         if (isBTReady) {
  75.             showInfo("初始化成功,等待连接");
  76.             this.removeCommand(com_pub);
  77.             this.addCommand(com_cancel);
  78.         } else {
  79.             showInfo("初始化失败,退出");
  80.             return;
  81.         }
  82.         // 生成服务端服务线程对象
  83.         processor = new ClientProcessor();
  84.         // ok, start accepting connections then
  85.         while (!isClosed) {
  86.             StreamConnection conn = null;
  87.             try {
  88.                 conn = notifier.acceptAndOpen();
  89.             } catch (java/io/IOException.java.html" target="_blank">IOException e) {
  90.                 // wrong client or interrupted – continue anyway
  91.                 continue;
  92.             }
  93.             processor.addConnection(conn);
  94.         }
  95.     }
  96.     public void publish() {
  97.         isClosed = false;
  98.         this.setString(null);
  99.         new java/lang/Thread.java.html" target="_blank">Thread(this).start();
  100.     }
  101.     public void cancelService() {
  102.         isClosed = true;
  103.         showInfo("服务终止");
  104.         this.removeCommand(com_cancel);
  105.         this.addCommand(com_pub);
  106.     }
  107.     /*
  108.      * (non-Javadoc)
  109.      * 
  110.      * @see javax.microedition.lcdui.CommandListener#commandAction(javax.microedition.lcdui.Command,
  111.      *      javax.microedition.lcdui.Displayable)
  112.      */
  113.     public void commandAction(Command arg0, Displayable arg1) {
  114.         if (arg0 == com_pub) {
  115.             //发布service
  116.             publish();
  117.         } else if (arg0 == com_cancel) {
  118.             cancelService();
  119.         } else {
  120.             cancelService();
  121.             midlet.showMainMenu();
  122.         }
  123.     }
  124.     
  125.     /**
  126.      * 内部类,服务端服务线程。
  127.      * @author Jagie
  128.      *
  129.      * TODO To change the template for this generated type comment go to
  130.      * Window – Preferences – Java – Code Style – Code Templates
  131.      */
  132.     private class ClientProcessor implements java/lang/Runnable.java.html" target="_blank">Runnable {
  133.         private java/lang/Thread.java.html" target="_blank">Thread processorThread;
  134.         private java/util/Vector.java.html" target="_blank">Vector queue = new java/util/Vector.java.html" target="_blank">Vector();
  135.         private boolean isOk = true;
  136.         ClientProcessor() {
  137.             processorThread = new java/lang/Thread.java.html" target="_blank">Thread(this);
  138.             processorThread.start();
  139.         }
  140.         public void run() {
  141.             while (!isClosed) {
  142.                 synchronized (this) {
  143.                     if (queue.size() == 0) {
  144.                         try {
  145.                             //阻塞,直到有新客户连接
  146.                             wait();
  147.                         } catch (java/lang/InterruptedException.java.html" target="_blank">InterruptedException e) {
  148.                         }
  149.                     }
  150.                 }
  151.                 
  152.                 //处理连接队列
  153.                 StreamConnection conn;
  154.                 synchronized (this) {
  155.                     if (isClosed) {
  156.                         return;
  157.                     }
  158.                     conn = (StreamConnection) queue.firstElement();
  159.                     queue.removeElementAt(0);
  160.                     processConnection(conn);
  161.                 }
  162.             }
  163.         }
  164.         
  165.         /**
  166.          * 往连接队列添加新连接,同时唤醒处理线程
  167.          * @param conn
  168.          */
  169.         void addConnection(StreamConnection conn) {
  170.             synchronized (this) {
  171.                 queue.addElement(conn);
  172.                 notify();
  173.             }
  174.         }
  175.     }
  176.     /**
  177.      * 从StreamConnection读取输入
  178.      * @param conn
  179.      * @return
  180.      */
  181.     private java/lang/String.java.html" target="_blank">String readInputString(StreamConnection conn) {
  182.         java/lang/String.java.html" target="_blank">String inputString = null;
  183.         try {
  184.             java/io/DataInputStream.java.html" target="_blank">DataInputStream dis = conn.openDataInputStream();
  185.             inputString = dis.readUTF();
  186.             dis.close();
  187.         } catch (java/lang/Exception.java.html" target="_blank">Exception e) {
  188.             e.printStackTrace();
  189.         }
  190.         return inputString;
  191.     }
  192.     
  193.     /**
  194.      * debug
  195.      * @param s
  196.      */
  197.     private void showInfo(java/lang/String.java.html" target="_blank">String s) {
  198.         java/lang/StringBuffer.java.html" target="_blank">StringBuffer sb = new java/lang/StringBuffer.java.html" target="_blank">StringBuffer(this.getString());
  199.         if (sb.length() > 0) {
  200.             sb.append("\n");
  201.         }
  202.         sb.append(s);
  203.         this.setString(sb.toString());
  204.     }
  205.     
  206.     /**
  207.      * 处理客户端连接
  208.      * @param conn
  209.      */    
  210.     private void processConnection(StreamConnection conn) {
  211.         // 读取输入
  212.         java/lang/String.java.html" target="_blank">String inputString = readInputString(conn);
  213.         //生成响应
  214.         java/lang/String.java.html" target="_blank">String outputString = inputString.toUpperCase();
  215.         //输出响应
  216.         sendOutputData(outputString, conn);
  217.         try {
  218.             conn.close();
  219.         } catch (java/io/IOException.java.html" target="_blank">IOException e) {
  220.         } // ignore
  221.         showInfo("客户端输入:" + inputString + ",已成功响应!");
  222.     }
  223.     
  224.     /**
  225.      * 输出响应
  226.      * @param outputData
  227.      * @param conn
  228.      */
  229.     private void sendOutputData(java/lang/String.java.html" target="_blank">String outputData, StreamConnection conn) {
  230.         try {
  231.             java/io/DataOutputStream.java.html" target="_blank">DataOutputStream dos = conn.openDataOutputStream();
  232.             dos.writeUTF(outputData);
  233.             dos.close();
  234.         } catch (java/io/IOException.java.html" target="_blank">IOException e) {
  235.         }
  236.     }
  237. }

小结

本文给出了一个简单的蓝牙服务的例子。旨在帮助开发者快速掌握JSR82.如果该文能对你有所启发,那就很好了。

参考资料

1. http://developers.sun.com/techtopics/mobility/apis/articles/bluetoothintro/
  JSR82 API介绍(英文)
2. asp?ArticleID=249">http://www.j2medev.com/Article/ShowArticle.asp?ArticleID=249
  JSR82 API 介绍(中文)
3. http://www.jcp.org/en/jsr/detail?id=82
  JSR82 Specification.
4.WTK22, BluetoothDemo 项目

作者简介

陈万飞,网名Jagie,培训师,爱好java技术.可通过chen_cwf@163.com与他联系