前言
由於項目需要,筆者需要在安卓平台開發一個程序,能夠用藍牙和下層的單片機通訊。
出於測試目的,筆者先用兩部均支持藍牙的安卓設備進行設備間通訊實驗,本文就是在這個實驗基礎上寫就的。
查閱了一些參考書籍和博客文章,筆者很是失望,因為它們都是羅列代碼(而且幾乎都是套用安卓官方自帶的那兩個例子——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>