Bluetoothで通信を行う(3)


Bluetoothで通信を行う(2)では、

  • 接続履歴のあるデバイス情報を取得
  • 接続したことのないデバイスを検出
  • 上記二つをリストに並べる

というところまでを解説しました。

今回は、リストに表示されたデバイスから一台を選択し、2台の端末間でデータ交換を行うところまでを解説します。
サンプルソースの動作としては、Bluetoothの接続が完了した際に、お互いの電話番号を交換し、表示用画面に遷移して表示するようにします。

※上サンプル画像では、個人情報保護のため、あえてEditTextに表示し、データ交換後に適当な電話番号に編集しておりますのであしからず。

2台の端末を用いて、Bluetoothの接続、およびデータ交換をする際のポイントは次の3つです

  1. 一台をサーバー、もう一台をクライアントとして処理を行う
  2. 接続待ちを別スレッドで行う
  3. 取得したソケットからインプットアウトプットストリームを取得してデータ交換を行う

以上の3つです。

上記1.については、2台の端末をサーバーとして待機させ、接続要求を投げた方をクライアントとして扱うという方法がありますので、今回はそちらを紹介します。

※なお、電話番号の取得方法についてはここでは詳しく解説しません。サンプルソースを参考にして頂くか、以下記事を参考にして下さい。

それでは続きをどうぞ、、、

サーバークラスの作成
まず初めに、サーバークラスの作成方法について解説します。
サーバー側の役割としては、クライアント側の接続要求を待ち続け、要求を受け取った際に、クライアント側のソケットを受け取ります。
ソースコードを見て下さい。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class BluetoothServerThread extends Thread {
    //サーバー側の処理
    //UUID:Bluetoothプロファイル毎に決められた値
    private final BluetoothServerSocket servSock;
    static BluetoothAdapter myServerAdapter;
    private Context mContext;
    //UUIDの生成
    public static final UUID TECHBOOSTER_BTSAMPLE_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
    public String myNumber;
 
    //コンストラクタの定義
    public BluetoothServerThread(Context context,String myNum, BluetoothAdapter btAdapter){
        //各種初期化
        mContext = context;
        BluetoothServerSocket tmpServSock = null;
        myServerAdapter = btAdapter;
        myNumber = myNum;
        try{
            //自デバイスのBluetoothサーバーソケットの取得
             tmpServSock = myServerAdapter.listenUsingRfcommWithServiceRecord("BlueToothSample03", TECHBOOSTER_BTSAMPLE_UUID);
        }catch(IOException e){
            e.printStackTrace();
        }
        servSock = tmpServSock;
    }
 
    public void run(){
        BluetoothSocket receivedSocket = null;
        while(true){
            try{
                //クライアント側からの接続要求待ち。ソケットが返される。
                receivedSocket = servSock.accept();
            }catch(IOException e){
                break;
            }
 
            if(receivedSocket != null){
                //ソケットを受け取れていた(接続完了時)の処理
                //RwClassにmanageSocketを移す
                ReadWriteModel rw = new ReadWriteModel(mContext, receivedSocket, myNumber);
                rw.start();
 
                try {
                    //処理が完了したソケットは閉じる。
                    servSock.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
    }
 
    public void cancel() {
            try {
                servSock.close();
            } catch (IOException e) { }
        }
}

Threadクラスを継承していることに注意して下さい。

コンストラクタの内部では、引数として渡されてきた自デバイスのBluetoothアダプタから、Bluetoothサーバーソケットを取得しています(20行目)。
第一引数には、アプリケーションを特定する任意の文字列を、第二引数には※プロファイルのUUIDをそれぞれ指定します。

run()内部では、取得したサーバーソケットを用いて接続要求待ちをしています(32行目)。
要求待ちにはaccept()メソッドを用います。クライアント側が要求に応えたとき、サーバーソケットからクライアント側より受け取ったソケットを返します

以降のデータ交換にはこのソケットを用いるため、サーバーソケットは不要となります。よって、close()メソッドを使用し、不要となったサーバーソケットを閉じています(45行目)。

なお、37〜50行目のif文内部では、接続完了後の処理を行っています。ReadWriteModelクラスについては、また後ほど解説します。

以上が、サーバー側の動作となります。

※プロファイル・・・Bluetoothには、デバイスによってそれぞれ対応したプロファイルが存在します。今回は、Androidで標準的にサポートされているSDAP(Service Discovery Application Profile)を利用しています。詳しくは以下のリンク(wiki)を参考にして下さい。

http://ja.wikipedia.org/wiki/Bluetooth

クライアントクラスの作成
次に、クライアントクラスの作成を行います。こちらもThreadクラスを継承しています。
内容としては非常にサーバークラスに似ています。ソースコードを見て下さい。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class BluetoothClientThread extends Thread {
    //クライアント側の処理
    private final BluetoothSocket clientSocket;
    private final BluetoothDevice mDevice;
    private Context mContext;
    //UUIDの生成
    public static final UUID TECHBOOSTER_BTSAMPLE_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
    static BluetoothAdapter myClientAdapter;
    public String myNumber;
 
    //コンストラクタ定義
    public BluetoothClientThread(Context context, String myNum , BluetoothDevice device, BluetoothAdapter btAdapter){
        //各種初期化
        mContext = context;
        BluetoothSocket tmpSock = null;
        mDevice = device;
        myClientAdapter = btAdapter;
        myNumber = myNum;
 
        try{
            //自デバイスのBluetoothクライアントソケットの取得
            tmpSock = device.createRfcommSocketToServiceRecord(TECHBOOSTER_BTSAMPLE_UUID);
        }catch(IOException e){
            e.printStackTrace();
        }
        clientSocket = tmpSock;
    }
 
    public void run(){
        //接続要求を出す前に、検索処理を中断する。
        if(myClientAdapter.isDiscovering()){
            myClientAdapter.cancelDiscovery();
        }
 
        try{
            //サーバー側に接続要求
            clientSocket.connect();
        }catch(IOException e){
             try {
                 clientSocket.close();
             } catch (IOException closeException) {
                 e.printStackTrace();
             }
             return;
        }
 
        //接続完了時の処理
        ReadWriteModel rw = new ReadWriteModel(mContext, clientSocket, myNumber);
        rw.start();
    }
 
    public void cancel() {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
      }
}

コンストラクタ内部の処理は、サーバー側とほぼ同じです。違いは、Bluetoothアダプタから取得しているソケットが、ここでははクライアントソケットだということです(メソッドの違いに注意)。

run()内部の処理も非常に似ています。クライアント側は、待ち受けるサーバー側に要求を出す側であるため、whileによるループは必要ありません。
37行目で、connect()メソッドを用いてサーバー側に接続要求を出しています。

ここで注意しないといけないのは、接続要求を出す前に、デバイスの検索処理を行っていた場合には、検索処理を中断する必要があります。
それをしないと接続処理が遅くなり、接続に失敗し易くなります。

以上がクライアント側の処理になります。

I/Oストリームを用いてデータのやり取りを行う
それでは、先ほどからちょこちょこ出てきていたReadWriteModelクラスについて解説します。
ReadWriteModelクラスでは、OutputStreamを用いたデータの書き込みと、InputStreamを用いたデータの読み込みを主に行います。
それではソースコードを見て下さい。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class ReadWriteModel extends Thread {
    //ソケットに対するI/O処理
 
    public static InputStream in;
    public static OutputStream out;
    private String sendNumber;
    private Context mContext;
 
    //コンストラクタの定義
    public ReadWriteModel(Context context, BluetoothSocket socket, String string){
        sendNumber = string;
        mContext = context;
 
        try {
            //接続済みソケットからI/Oストリームをそれぞれ取得
            in = socket.getInputStream();
            out = socket.getOutputStream();
        } catch (IOException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
    }
 
    public void write(byte[] buf){
        //Outputストリームへのデータ書き込み
        try {
            out.write(buf);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
 
    public void run() {
        byte[] buf = new byte[1024];
        String rcvNum = null;
        int tmpBuf = 0;
 
        try {
            write(sendNumber.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
        while(true){
            try {
                tmpBuf = in.read(buf);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            if(tmpBuf!=0){
                try {
                    rcvNum = new String(buf, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
 
            Intent i = new Intent(mContext, StreamActivity.class);
            i.putExtra("NUMBER", rcvNum);
            mContext.startActivity(i);
        }
    }
}

ストリームへのI/O処理も、Threadを用いてバックグラウンドで行うのが適切でしょう。むしろUIスレッド(Activityなど)で行うべきではありません。
まずコンストラクタの内部で、接続済みソケットからInputStreamおよびOutputSteamを取得しています(16,17行目)。
write()関数内部では、取得したOutputStreamに対して、書き込みを行っています(実際の書き込みはrun()内部の最初に行う)。
ちなみにここでは、引数として渡されてきた自身の電話番号を書き込むことになります。

run()内部では、自身の電話番号のデータをwrite()関数にて書き込んだ後(35行目)、通信相手によって書き込まれた相手の電話番号のデータを、Inputストリームから読み込みます(48行目)。

最後に、取得した文字列を、電話番号表示用画面にIntentを用いて表示用画面に渡しつつ画面遷移しています。

以上がI/O処理となります。

まとめ
最後に、今日の全体の流れを、サンプルアプリの動きを追いながら、前回までとのソースコードの差異も含めてまとめてみます。
●前回、DeviceListActivity.javaにて、接続可能なデバイスリストの作成を行いました。
[写真]
●今回、DeviceListActivity.javaのonResume()にて、サーバースレッドを起動し、クライアントの接続を待ち受けを開始しました。

1
2
3
4
5
6
7
@Override
public void onResume() {
    super.onResume();
    //サーバースレッド起動、クライアントのからの要求待ちを開始
BluetoothServerThread BtServerThread = new BluetoothServerThread(this, myNumber , mBtAdapter);
BtServerThread.start();
}

サーバースレッド内では、自デバイスのサーバーソケットを取得し、connect()メソッドを用いて接続要求待ちを行いました。
●リストから任意のデバイスが選択された際に、クライアント側のスレッドを開始しました。

1
2
3
4
5
6
7
8
9
//デバイスリスト選択時の処理
@Override
public void onItemClick(AdapterView<!--?--> parent, View v, int position, long id) {
    // TODO Auto-generated method stub
    ListView listView = (ListView) parent;
    BluetoothDevice device = foundDeviceList.get(offSet + position);
    BtClientThread = new BluetoothClientThread(mContext, myNumber, device, mBtAdapter);
    BtClientThread.start();
}

つまり、リストを選択した方のデバイスがクライアントとして、サーバー側に接続要求を出す事になります。
クライアントスレッド側では、自デバイスのクライアントソケットを取得し、connect()メソッドを用いて、サーバー側に接続要求を出しました。

このとき、選択したデバイスが接続履歴のないデバイスだった場合、下図のようなダイアログが表示されますので、ペアを設定するを選択してください。

●クライアント側の接続要求を受けて、接続が完了したら、ReadWriteModelを呼び出し、データのI/O処理を行いました。

●最後に、取得した通信相手の電話番号を表示するためのActivityにIntentを用いて遷移して通信終了です。

どうでしょう、全体像をイメージして頂けたでしょうか。

これまで、全3回に分けて、

  • Bluetoothの有効化
  • 接続可能デバイスのリスト作成
  • 任意のデバイスとのデータ交換

についてそれぞれ解説してきました。
また機会があれば、今回とは違った使い方を解説できればと思います。

※なお、サンプルコードにつきましてはデバッグ等は厳密には行っておりません。サンプルコードの流用による動作につきましては保証致しかねますので、自己責任にてお願いを致します。