Sunday, April 15, 2012

從BluetoothChat範例程式學藍芽連線原理

撰寫時間︰2012/04/15 16:06
更新時間︰2012/04/14 10:34
文章更新次數︰1

一、前言
Bluetooth藍芽裝置在手機界已經存在很久了,
也早就成為連低階Android手機都有的基本配備。
官方也提供了一個利用藍芽連線互相對話的範例程式
讓我們能快速地了解藍芽在Android中的使用方式。
 
二、文章開始
首先,
我們先建立一個觀念︰
藍芽一定分成2個端點,
分別為被動的Server端主動的Client端

底下是BluetoothChat的Sample Code程式流程︰

=========現在在BluetoothChat.java底下==============
BluetoothChat.java是這個範例程式的主頁面,可以在跟已連線的藍芽設備對話。

1.在onCreate()時,呼叫
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 
一起來看看官方文件裡怎麼說BluetoothAdapter
BluetoothAdapter represents the local Bluetooth adapter (Bluetooth radio).
The BluetoothAdapter is the entry-point for all Bluetooth interaction.
Using this, you can discover other Bluetooth devices, query a list of bonded (paired) devices, instantiate a BluetoothDevice using a known MAC address, and create a BluetoothServerSocket to listen for communications from other devices.
BluetoothAdapter是區域藍芽接口(藍芽廣播)。BluetoothAdapter也是所有藍芽交易互動的啟始點。用這個接口,我們可以偵測區域內有哪些其它的藍芽裝置、查詢已配對過的藍芽列表、用已知的MAC地址建立一個BluetoothDevice實體、建立一個BluetoothServerSocket來監聽是否有其它藍芽裝置傳來的通訊…等。

2-1.得到一個BluetoothAdapter實體之後,
在onStart()裡,
如果沒有啟動藍芽,則要求使用者開啟藍芽。
指令是︰
        
        if (!mBluetoothAdapter.isEnabled()) {
            Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
        // Otherwise, setup the chat session
        }else {
            if (mChatService == null) setupChat();
        }
 

2-2.透過setupChat()建立起基本的對話視窗和BluetoothChatService背景服務,並把主Thread的Handler傳給Service以供日後傳回message。

            mChatService = new BluetoothChatService(this, mHandler);
 

3.在onResume()裡,也做一樣的事,如果檢查沒有開啟藍芽BluetoothChatService背景服務,則再次開始該服務。
if (mChatService.getState() == BluetoothChatService.STATE_NONE) {
              // Start the Bluetooth chat services
              mChatService.start();
            }
 

=========現在進入BluetoothChatService.java裡==============

程式碼才剛開出來,
馬上就看到這個Service的程式架構中,塞了3個執行緒,
分別為︰
(1)AcceptThread
(2)ConnectThread
(3)ConnectedThread

馬上來看看它們在Service裡,分別擔任什麼樣的任務︰
// Cancel any thread attempting to make a connection
        if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}

        // Cancel any thread currently running a connection
        if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}

        // Start the thread to listen on a BluetoothServerSocket
        if (mAcceptThread == null) {
            mAcceptThread = new AcceptThread();
            mAcceptThread.start();
        }
 
1.在onStart()中,
檢查如果ConnectThread和ConnectedThread存在,則將他們關掉。
2.啟動一個AcceptThread(現在的流程是在藍芽開啟中的狀態,開啟了一個AcceptThread待命)。
這個AcceptThread存在的目的,是因為程式先假設每臺裝置都有可能想要跟它做藍芽連線。

來看一下這個程式一啟動後就執行的AcceptThread裡面做了些什麼︰
  /**
     * This thread runs while listening for incoming connections. It behaves
     * like a server-side client. It runs until a connection is accepted
     * (or until cancelled).
     */
    private class AcceptThread extends Thread {
        // The local server socket
        private final BluetoothServerSocket mmServerSocket;

        public AcceptThread() {
            BluetoothServerSocket tmp = null;

            // Create a new listening server socket
            try {
                tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
            } catch (IOException e) {
                Log.e(TAG, "listen() failed", e);
            }
            mmServerSocket = tmp;
        }

        public void run() {
            if (D) Log.d(TAG, "BEGIN mAcceptThread" + this);
            setName("AcceptThread");
            BluetoothSocket socket = null;

            // Listen to the server socket if we're not connected
            while (mState != STATE_CONNECTED) {
                try {
                    // This is a blocking call and will only return on a
                    // successful connection or an exception
                    socket = mmServerSocket.accept();
                } catch (IOException e) {
                    Log.e(TAG, "accept() failed", e);
                    break;
                }

                // If a connection was accepted
                if (socket != null) {
                    synchronized (BluetoothChatService.this) {
                        switch (mState) {
                        case STATE_LISTEN:
                        case STATE_CONNECTING:
                            // Situation normal. Start the connected thread.
                            connected(socket, socket.getRemoteDevice());
                            break;
                        case STATE_NONE:
                        case STATE_CONNECTED:
                            // Either not ready or already connected. Terminate new socket.
                            try {
                                socket.close();
                            } catch (IOException e) {
                                Log.e(TAG, "Could not close unwanted socket", e);
                            }
                            break;
                        }
                    }
                }
            }
            if (D) Log.i(TAG, "END mAcceptThread");
        }

        public void cancel() {
            if (D) Log.d(TAG, "cancel " + this);
            try {
                mmServerSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of server failed", e);
            }
        }
    }
 
我們看到了在AcceptThread裡面,埋入了一個BluetoothServerSocket。
看一下Bluetooth Android官方對它的解釋︰
BluetoothServerSocket represents an open server socket that listens for incoming requests (similar to a TCP ServerSocket). In order to connect two Android devices, one device must open a server socket with this class. When a remote Bluetooth device makes a connection request to the this device, the BluetoothServerSocket will return a connected BluetoothSocket when the connection is accepted.
BluetoothServerSocket是一個開放式的server socket,用來監聽任何傳進來的請求(原理類似TCP ServerSocket)。為了讓2隻Android devices能夠連線,其中一隻裝置必須開啟server socket。當遠端的藍芽裝置向手上這隻裝備請求連線後,這隻裝置上的BluetoothServerSocket會回傳一個accepted的BluetoothSocket給呼叫那一方。

因此我們知道,上面程式碼中
BluetoothSocket  socket = mmServerSocket.accept();
 
就是應證了BluetoothServerSocket會吐BluetoothSocket出來這件事。

回到一開始呼叫AcceptThread.start()的那個時間點,
也就是說,
程式在一啟動時,
都先要求使用者開啟藍芽,
然後隨時準備接收別臺藍芽裝置會傳送連線請求的事件。

我們取到了BluetoothSocket後,
看看這個BluetoothSocket能做些什麼。

首先,
在官方技術文件提到︰
BluetoothSocket represents the interface for a Bluetooth socket (similar to a TCP Socket). This is the connection point that allows an application to exchange data with another Bluetooth device via InputStream and OutputStream.
BluetoothSocket是一個Bluetooth socket的接口(原理類似TCP Socket)。這個連結點允許APP透過InputStream和OutpusStream互相交換資料。

因此我們得知,
BluetoothSocket可以讓我們做到資料交換的功能。

因為在Service onStart()呼叫AcceptThread.start()後,
馬上將藍芽狀態設定成setState(STATE_LISTEN);
因此,在switch迴圈中,
程式執行了connected()函式。

這段程式碼如下︰
                        case STATE_LISTEN:
                        case STATE_CONNECTING:
                            // Situation normal. Start the connected thread.
                            connected(socket, socket.getRemoteDevice());
                            break;

馬上來看看connected()函式做了哪些事
    /**
     * Start the ConnectedThread to begin managing a Bluetooth connection
     * @param socket  The BluetoothSocket on which the connection was made
     * @param device  The BluetoothDevice that has been connected
     */
    public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
        if (D) Log.d(TAG, "connected");

        // Cancel the thread that completed the connection
        if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}

        // Cancel any thread currently running a connection
        if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}

        // Cancel the accept thread because we only want to connect to one device
        if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}

        // Start the thread to manage the connection and perform transmissions
        mConnectedThread = new ConnectedThread(socket);
        mConnectedThread.start();

        // Send the name of the connected device back to the UI Activity
        Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_DEVICE_NAME);
        Bundle bundle = new Bundle();
        bundle.putString(BluetoothChat.DEVICE_NAME, device.getName());
        msg.setData(bundle);
        mHandler.sendMessage(msg);

        setState(STATE_CONNECTED);
    }
 
為了避免重覆連線,
先檢查有沒有已存在的ConectThread、ConnectedThrad和AcceptThread。
如果有,一律先關掉。
然後,啟動ConnectedThread,
並將MESSAGE_DEVICE_NAME用handler(mHandler,還記得我們前面有提到在BluetoothChat.java傳了一個主Thread的Handler給Service嗎?)傳訊的方式,
將Client端的裝置資料傳回BluetoothChat.java,
讓Server端知道是誰在跟它做連結。

前面提到,
一旦取得了BluetoothSocket之後,
就可以開始執行互相傳遞資料的工作了 。
這個被啟動的ConnectedThread就是在做資料互傳的監聽工作
我們看看ConnectedThread做了些什麼
   /**
     * This thread runs during a connection with a remote device.
     * It handles all incoming and outgoing transmissions.
     */
    private class ConnectedThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;

        public ConnectedThread(BluetoothSocket socket) {
            Log.d(TAG, "create ConnectedThread");
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the BluetoothSocket input and output streams
            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "temp sockets not created", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        public void run() {
            Log.i(TAG, "BEGIN mConnectedThread");
            byte[] buffer = new byte[1024];
            int bytes;

            // Keep listening to the InputStream while connected
            while (true) {
                try {
                    // Read from the InputStream
                    bytes = mmInStream.read(buffer);

                    // Send the obtained bytes to the UI Activity
                    mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
                            .sendToTarget();
                } catch (IOException e) {
                    Log.e(TAG, "disconnected", e);
                    connectionLost();
                    break;
                }
            }
        }

        /**
         * Write to the connected OutStream.
         * @param buffer  The bytes to write
         */
        public void write(byte[] buffer) {
            try {
                mmOutStream.write(buffer);

                // Share the sent message back to the UI Activity
                mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                Log.e(TAG, "Exception during write", e);
            }
        }

        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of connect socket failed", e);
            }
        }
    }
 
是的!
有看到嗎?
ConnectedThread正在用BluetoothSocket取得InputStream和OutputStream,
並透過旗下的write()和read()在做2隻藍芽裝置的溝通!

現在剩下ConnectThread還沒有去理解了,
查了一下ConnectThread被start的時間是發生在一開始對話頁面的menu鍵中!
原來ConnectThread的目的是要主動連接其它已開啟藍芽的裝置。
當使用者點擊Connect a device時,
會啟動ConnectThread,
開始尋找附近的藍芽裝置,
並對對方發出連線訊號,
對方監聽到你的配對要求後,
對方手機裡原本程式就開啟中的AcceptThread便答應你的請求,
然後開啟ConnectedThread,
並利用連結成功後得到的BluetoothSocket和你做藍芽傳輸溝通。

主動連線端是Client端,被動接收端是Server端,
就好像精子與受精卵…

三、總結
在這裡我把整個程式流程重覆敍述一次︰

在連線的一開始,兩隻手機的程式一開始都先建立一個AcceptThread
(因為誰都不知道誰最後會成為被動接收的Server端,誰又是主動的Client端),
然後都跟RFCOMM頻道索取這隻app專屬的BluetoothServerSocket實體。

Server方做了些什麼︰
用BluetoothServerSocket這個實體去等待Client端用ConnectThread發出的請求連線事件
連線若成功會得到這次藍芽溝通專用的BluetoothSocket。

Client方做了些什麼︰
Client端執行ConnectThread
1.Client端在與Server方連線(Connect a deivce)之前,
會先取得到Server端的身份證MAC address,
並用該address得到Server端的BluetoothDevice實體。
2.Client端藉由自己的MY_UUID和Server端的BluetoothDevice實體,
從RFCOMM頻道拿到這次藍芽溝通專用的BluetoothSocket。

兩方在這個時候都拿到這次藍芽溝通專用的BluetootheSocket
也都在此時知道了對方的BluetoothDevice實體(知道對方的身份)。
這時候雙方都同時開啟ConnectedThread,
彼此利用BluetoothSocket互相做資料傳輸。

註︰資料傳輸利用 InputStream和OutputStream。

41 comments:

陳楷明 said...

你好 可否提供完整範例檔 學生專題用

C^3 said...

你好!
我使用
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();時,出現了以下的兩個錯誤:
1.android.bluetooth.BluetoothAdapter- Call requires API level 5 (current min is 3):
2.android.bluetooth.BluetoothAdapter#getDefaultAdapter
請問這是甚麼意思?跟編譯版本有關嗎?

Anonymous said...

C^3你修改AndroidManifest.xml

然後Clean project試試。

Anonymous said...

C^3你修改AndroidManifest.xml
""
然後Clean project試試。

Anonymous said...

C^3你修改AndroidManifest.xml
uses-sdk minSdkVersion="10"
然後Clean project試試。

C^3 said...

成功了,感謝!
另外還有幾個問題:
Android藍芽傳輸只能轉換成byte嗎?
可以直接傳字串或字元嗎?
因為我要將指令傳給ARM的STM32F217晶片做處理,Android是用byte來傳值讓我很困擾。

Chris CY said...

您好 小鰻大大 可以請問您一個問題 指導小弟嘛
就是在IAB 這個機制中
是手機Client 這邊 要發送訊息給後台server
再由後台Server跟Google 溝通
還是 直接由 Client 跟 google 溝通呢

另外 就是 假如說 購買一個物件 購買成功之後
是我們自己在開通那個物件 還是 我們必須要將物件上傳到 google 再由他們丟回給我們呢

這是小的問題 希望大大能指導我一下 謝謝

Ping - Ling Yuan said...

您好 請問我有方法讓只要有安裝相同程式的使用者

他們之間的藍芽連結是不需要再額外詢問的嗎?

林則宇 said...

請問一下
關於學習android的路程
直接看範例 實在有許多不知道從哪裡跑出來的變數 或是引用

是否要從書上學習比較了解

潘小鰻 said...

@ 林則宇 最好的方法是看能不能找到網路上有sample code,這樣會最快幫到你

張瑜 said...

官方的範例打開後,連完藍芽應用程式發生錯誤
這個跟API版本有關係?

k ai Chiu said...

請問一下,我用bluetoothchat 範例,因為看起來跟你發布的code類似,所以請問看看,因為我手機跟其他人手機可以用這範例app互傳訊息,但是我手機配對其他藍芽模組可以配對,但是用這程式在連接那邊一直無法連接,請問這跟app code 有關係,還是藍芽模組有關係,如果可以就回我信箱 chiuka12@gmail.com ,謝謝!!如果需要我在把程式寄給你,謝謝

Unknown said...

想要問問 小鰻大大你是用Eclipse
程式做的嗎? 因為要做專題但是以前沒接觸過這東西 但我想做的東西 就是從8051接收到訊號後 傳到手機顯示當前狀態 就這樣。

張竣皓 said...

請問一下,要如何讓bluetooth在手機背景程式抓資料呢?

gmail:brian.chunhao@gmail.com

張竣皓 said...
This comment has been removed by the author.
張竣皓 said...
This comment has been removed by the author.
張竣皓 said...

請問一下,要如何讓bluetooth在手機背景程式抓來自其他裝置的資料(get byte)呢?

Jason Chang said...

您好~
可以提供範例檔嗎?
專題需要用到
謝謝您~
iva98741@gmail.com

黃吉村 said...

可以提供完整CODE 嗎 包刮 LAYOUT的部分謝謝 專題需要
g062567512@gmail.com

Chia Carrie said...

hi 請問能 提供範例檔讓我學習用嗎 ...
kaka784804@hotmail.com

曾靖雯 said...
This comment has been removed by the author.
曾靖雯 said...

可否提供完整檔案練習用
u0251069@gmail.com

Anonymous said...

請問能提供完整檔案給我練習嗎?
謝謝~
a22658802@gmail.com

阿阿寶 said...

您好!
請問可以提供完整範例檔嗎?
專題需要用到
不好意思
謝謝您
h930650806@gmail.com

Lin Lee said...

您好:
我目前正在研究Android wear 間藍芽的傳輸 (使用motorola 穿戴式裝置)
請問方便提供完整的範例檔嗎?
非常謝謝您!!!
qzwx74@gmail.com

劉育涵 said...

你好:
我根據版大的教學,寫出了一藍芽傳輸程式,但是只能跟BLUETOOTH V2.0 連線,
V4.0 會出現 Unable to connect device ,請問要如何針對V4.0連線部分進行更改,能否給些建議,謝謝。

Wei-Hau Chen said...

您好,
請問是否方便提供完整範例檔給我學習嗎?
感謝!
Howard1014@gmail.com

luffy roan said...

不會吧..用byte超正常的

Carol Jheng said...

你好!
請問可以提供完整的範例檔,給我學習嗎?
謝謝您
jhengko1116@gmail.com

電機工程系林詩凱 said...

您好 !
請問可以提供 完整的code給我參考嗎
專題卡住了 感謝您
40125115@gm.nfu.edu.tw

吳俊毅 said...

你好,請問可以提供完整範例檔給我參考嗎,學生專題用,謝謝。
ap126126@gmail.com

林信宇 said...

您好 !
請問可以提供 完整的code給我參考嗎
麻煩您了!!
a5030755662000@yahoo.com.tw

蔡林宸 said...

你好!
請問可以提供完整的範例檔,給我學習嗎?
謝謝您
james0716boy@gmail.com

孝國呂 said...

您好:
我目前正在研究Android wear 間藍芽的傳輸
請問方便提供完整的範例檔嗎?
非常謝謝您!!!
changhua21015@gmail.com

Sir Yang said...

我目前正在做 Android & Arduino 間藍芽的傳輸 的相關專題
請問方便提供完整的範例檔嗎?
真的非常謝謝您
letsgo911488@gmail.com

Anonymous said...

非常詳細
不知道能不能做到多人同時藍芽連接呢?

小羽 said...

你好~
最近正在學習跟android 藍芽相關的問題
能否方便提供完整範例檔呢?
非常感謝你
L0972805195@gmail.com

川諺陳 said...


我剛從android studio 開始學起
有點迷迷糊糊藍芽方面那些該放在哪些地方
可以提供source code讓我參考嗎
可以的話感謝你
這是我的mail
aboy0507@gmail.com

川諺陳 said...


我剛從android studio 開始學起
有點迷迷糊糊藍芽方面那些該放在哪些地方
可以提供source code讓我參考嗎
可以的話感謝你
這是我的mail
aboy0507@gmail.com

Unknown said...

你好
因為學校專題需要有手機APP藍牙連接的部分
不過才剛碰Android Studio而已
請問可以提供code參考一下嗎

這是我的Email
star881113@gmail.com

可以的話謝謝
也感謝你的各種教學!

Unknown said...

Unknown said...
你好
因為學校專題需要有手機APP藍牙連接的部分
但輸入上述程式碼obtainMessage一直有誤
不太知道為什麼QQQQQ
請問可以提供code參考一下嗎

這是我的Email
lafukanim@gmail.com

謝謝!