前言
由于项目需要,笔者需要在安卓平台开发一个程序,能够用蓝牙和下层的单片机通讯。
出于测试目的,笔者先用两部均支持蓝牙的安卓设备进行设备间通讯实验,本文就是在这个实验基础上写就的。
查阅了一些参考书籍和博客文章,笔者很是失望,因为它们都是罗列代码(而且几乎都是套用安卓官方自带的那两个例子——API Guides下的蓝牙指导代码和samples下的BluetoothChat),但是并没有给出系统的宏观分析,让人颇感失望。
认真研读API Guides下的蓝牙指导代码后,笔者先绘制出了类图,然后在其指导下完成了整个实验程序的构建,其间当然多有反复与修改,作为程序员大家肯定都懂的——“三分编程,七分调试”。
背景知识
1.蓝牙是什么?
一种近距离无线通信协议,小功率蓝牙设备(一般我们见到的都是)的通讯速率约为1Mbps,通讯距离为10m。
2.蓝牙分主从吗?
分的,蓝牙组网的方式是:1主(<8)从。蓝牙组的网有个可爱的名字——“微微网”(piconet),蓝牙设备在微微网的地址(注意不是mac地址)是3位,因此一个微微网最多有8台被激活设备。设备的主从角色的分配是在组成微微网时临时确定的,不过蓝牙技术支持“主从转换”。
3.蓝牙设备都有哪些状态?
激活,呼吸,保持,休眠:功率依次递减。
框架
我们先来看看一般的通讯模型是怎样的
打开-》建立连接-》通讯-》断开连接-》关闭
打开设备是一切工作的前提,建立连接需要保证两个蓝牙设备之间的可见性而搜索就是找寻周围的蓝牙设备(此操作比较占带宽,通讯时务必关掉),通讯就是把两个设备用某种方式连接起来(一主一从)然后发送消息与接收消息,最后需要断开连接,关闭设备。
据此,设计UI如下(接收消息按钮仅仅是为了美观,代码中并未实现什么功能):
这个程序仅用到了一个活动:
//实现OnClickListener接口是一个技巧,这样在活动中给控件设置监听的时候直接传this就好了,代码会简洁许多
比较重要的是三个内部类:
//这三个都是线程类,和蓝牙相关的会阻塞的操作都封装在它们中
代码
代码写的不是很完善,但完全能够达到测试的功能
建议把代码复制下来,再用IDE工具查看,先看框架(outline),再看细节
//看代码有迷惑的地方,再去看前面的类图,在树林中迷了路,此时需要登高四望
//所有的输出均会打印到logcat中,用System.out过滤一下
//注意:使用蓝牙,需要声明BLUETOOTH权限,如果需要扫描设备或者操作蓝牙设置,则还需要BLUETOOTH_ADMIN权限,本实验两个权限都需要
测试
1.拿出两台设备,安装好程序,完成配对//配对的过程需要人为操作,与这个程序没有关系
2.两台设备均打开蓝牙(从系统界面或是这个程序的“打开蓝牙按钮均可”)
3.一台设备通过adb连接到电脑,点击“启动主机”按钮
4.在另一台设备点击“启动从机”按钮
5.在这台设备点击“发送消息”按钮
6.不出意外的话,logcat的System.out会打印出三条记录——1,2,3
package com.example.testbluetooth; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Set; import java.util.UUID; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.app.Activity; import android.widget.Button; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.view.View; import android.view.View.OnClickListener; import android.widget.Toast; public class MainActivity extends Activity implements OnClickListener { //蓝牙 BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); //蓝牙状态 final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {//接收蓝牙发现的消息 @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BluetoothDevice.ACTION_FOUND.equals(action)) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); System.out.println("From mBroadcastReceiver:"+device.getName() + "-" + device.getAddress()); } } }; //消息处理 private static final int MESSAGE_READ = 0; private Handler mHandler = new Handler(){ public void handleMessage(Message msg){ switch(msg.what){ case MESSAGE_READ: byte[] buffer = (byte[])msg.obj;//buffer的大小和里面数据的多少没有关系 for(int i=0; i<buffer.length; i++){ if(buffer[i] != 0){ System.out.println(buffer[i]); } } break; } } }; //线程 ConnectedThread mConnectedThread; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //控件 findViewById(R.id.open).setOnClickListener(this); findViewById(R.id.close).setOnClickListener(this); findViewById(R.id.search).setOnClickListener(this); findViewById(R.id.server).setOnClickListener(this); findViewById(R.id.client).setOnClickListener(this); findViewById(R.id.send).setOnClickListener(this); findViewById(R.id.receive).setOnClickListener(this); findViewById(R.id.paired).setOnClickListener(this); //注册广播 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); registerReceiver(mBroadcastReceiver, filter); // Don't forget to unregister during onDestroy } // a fictional method in the application //that will initiate the thread for transferring data void manageConnectedSocket(BluetoothSocket socket){//不知何故,但是在此处用Toast会出现Looper问题//Toast.makeText(this, "A socket opened!", Toast.LENGTH_SHORT).show(); //System.out.println("From manageConnectedSocket:"+socket); mConnectedThread = new ConnectedThread(socket); mConnectedThread.start(); } @Override public void onClick(View v) { // TODO Auto-generated method stub //设备支持不支持蓝牙和有没有插蓝牙芯片没有关系,这个是操作系统的事情 //如果系统不支持蓝牙,会返回空,经测试,即使没有蓝牙芯片,bluetooth的值可为非空 //但是如果没有插蓝牙芯片,系统会阻塞住 switch (v.getId()) { case R.id.open: if (!mBluetoothAdapter.isEnabled()) {//如果蓝牙没有打开 mBluetoothAdapter.enable();//这种打开方式可以跳过系统打开蓝牙的界面 } break; case R.id.close: if (mBluetoothAdapter.isEnabled()) {//如果蓝牙已经打开 mBluetoothAdapter.disable(); } break; case R.id.search: if (!mBluetoothAdapter.isDiscovering()) {//如果蓝牙不处于搜索状态(即找寻附近蓝牙) mBluetoothAdapter.startDiscovery();//Enabling discoverability will automatically enable Bluetooth. } break; case R.id.server: new AcceptThread().start(); ((Button)findViewById(R.id.client)).setVisibility(View.INVISIBLE); break; case R.id.client: for(BluetoothDevice device:pairedDevices){ new ConnectThread(device).start(); ((Button)findViewById(R.id.server)).setVisibility(View.INVISIBLE); } break; case R.id.send: if(mConnectedThread != null){ byte[] bytes = new byte[]{1,2,3}; mConnectedThread.write(bytes); } break; case R.id.receive: break; case R.id.paired: pairedDevices = mBluetoothAdapter.getBondedDevices(); for(BluetoothDevice device:pairedDevices){ System.out.println("From paired:"+device.getName()); } break; default: break; } } private class AcceptThread extends Thread { private final BluetoothServerSocket mmServerSocket; public AcceptThread() { // Use a temporary object that is later assigned to mmServerSocket, // because mmServerSocket is final BluetoothServerSocket tmp = null; try { // MY_UUID is the app's UUID string, also used by the client code tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord("blind_nav", UUID.fromString("0c312388-5d09-4f44-b670-5461605f0b1e")); } catch (IOException e) { } mmServerSocket = tmp; } public void run() { BluetoothSocket socket = null; // Keep listening until exception occurs or a socket is returned while (true) { try { //System.out.println("From AcceptThread:"); socket = mmServerSocket.accept(); } catch (IOException e) { break; } // If a connection was accepted if (socket != null) { // Do work to manage the connection (in a separate thread) manageConnectedSocket(socket); try { mmServerSocket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } break; } } } /** Will cancel the listening socket, and cause the thread to finish */ public void cancel() { try { mmServerSocket.close(); } catch (IOException e) { } } } private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { // Use a temporary object that is later assigned to mmSocket, // because mmSocket is final BluetoothSocket tmp = null; mmDevice = device; // Get a BluetoothSocket to connect with the given BluetoothDevice try { // MY_UUID is the app's UUID string, also used by the server code tmp = device.createRfcommSocketToServiceRecord(UUID.fromString("0c312388-5d09-4f44-b670-5461605f0b1e")); } catch (IOException e) { } mmSocket = tmp; } public void run() { // Cancel discovery because it will slow down the connection mBluetoothAdapter.cancelDiscovery(); try { // Connect the device through the socket. This will block // until it succeeds or throws an exception mmSocket.connect();//这个操作需要几秒钟,不是立即能见效的 } catch (IOException connectException) { // Unable to connect; close the socket and get out try { mmSocket.close(); } catch (IOException closeException) { } return; } // Do work to manage the connection (in a separate thread) manageConnectedSocket(mmSocket); } /** Will cancel an in-progress connection, clean up all internal resources, and close the socket */ public void cancel() { try { mmSocket.close(); } catch (IOException e) { } } } private class ConnectedThread extends Thread { private final BluetoothSocket mmSocket; private final InputStream mmInStream; private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket) { mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; // Get the input and output streams, using temp objects because // member streams are final try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { } mmInStream = tmpIn; mmOutStream = tmpOut; } public void run() { byte[] buffer = new byte[1024]; // buffer store for the stream int bytes; // bytes returned from read() // Keep listening to the InputStream until an exception occurs while (true) { try { // Read from the InputStream bytes = mmInStream.read(buffer); // Send the obtained bytes to the UI activity //Message msg = new Message(); //msg.what = MESSAGE_READ; mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } catch (IOException e) { break; } } } /* Call this from the main activity to send data to the remote device */ public void write(byte[] bytes) { try { mmOutStream.write(bytes); } catch (IOException e) { } } /* Call this from the main activity to shutdown the connection */ public void cancel() { try { mmSocket.close(); } catch (IOException e) { } } } }
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/open" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="打开蓝牙" /> <Button android:id="@+id/close" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="关闭蓝牙" /> </LinearLayout> <Button android:id="@+id/search" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="搜索蓝牙" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/server" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="启动主机" /> <Button android:id="@+id/client" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="启动从机" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/send" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="发送消息" /> <Button android:id="@+id/receive" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="接收消息" /> </LinearLayout> <Button android:id="@+id/paired" android:layout_height="wrap_content" android:layout_width="match_parent" android:text="已配对蓝牙" /> </LinearLayout>