Android 間での RFCOMM チャネルの Bluetooth 通信 2/2

前の記事「Android 間での RFCOMM チャネルの Bluetooth 通信 1/2」では、 Android における Bluetooth 通信の方法を説明しました。

ポイントはサーバー側とクライアント側があり、サーバー側が接続を待ち受け、クライアントが接続要求を出す。

通信が確立されると最終的に BluetoothSocket オブジェクトが取得できるので、それを用いてデータ通信を行うということです。

デバイスを見つけたり、UUID で SDP レコードを探す、というあたりは Bluetooth 特有の話題ですが、いったんソケットがとれると TCP/IP 上の通信と変わらぬやりとりができます。

さて、ここでは具体的なサンプルを動かしてみましょう。世の中に複雑なサンプルはあふれていますので、ここではなるべく簡略化します。

サーバー・プログラムには EditText がひとつあるだけです。

クライアント・プログラムはサーバー・プログラムと Bluetooth 接続したら、あるコマンドを送ります。 サーバー・プログラムはコマンドを受け取ったら EditText に入力された文字をクライアントに返します。

クライアント・プログラムはサーバーから返された文字を画面に表示します。

クライアント側には簡単に接続の状態も記録しています。

これはイメージ的にはサーバー側が 「温度計となる IoT デバイス」 で、クライアント側は「温度の表示パネル」といったイメージです。 このため、コード内でクライアントからサーバーに投げるコマンドは "GET:TEMP" (温度の取得) という風にしてます。

サーバー側は "GET:TEMP" という文字を受け取ったら、サーバーに文字を送り返しています。

サーバー・プログラムの実装

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.keicode.android.testapps.bttest1s">

    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.BLUETOOTH"/>

    <application
        android:name=".BTServerApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Bluetooth のパーミッション設定をしています。

その他、グローバル変数的なデータストレージを用意するためアプリケーションのクラス名を指定して、自前のアプリケーションクラスを利用しています。

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.keicode.android.testapps.bttest1s.MainActivity">
    <EditText
        android:id="@+id/tempEditText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="25" />
</RelativeLayout>

EditText をひとつ配置しているだけです。

MainActivity.java

package com.keicode.android.testapps.bttest1s;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.widget.EditText;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class MainActivity extends AppCompatActivity {

    static final String TAG = "BTTest1S";

    BluetoothAdapter bluetoothAdapter = null;

    EditText tempEditText;
    BTServerThread btServerThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Find Views
        tempEditText = (EditText)findViewById(R.id.tempEditText);
        tempEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
            @Override
            public void afterTextChanged(Editable editable) {
                String s = editable.toString();
                ((BTServerApplication) MainActivity.this.getApplication()).setTempValue(s);
            }
        });

        if(savedInstanceState != null){
            tempEditText.setText(savedInstanceState.getString(Constants.STATE_TEMP));
        }
    }

    @Override
    protected void onResume(){
        super.onResume();

        if(bluetoothAdapter == null) {
            bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
            if (bluetoothAdapter == null) {
                Log.d(TAG, "This device doesn't support Bluetooth.");
            }
        }

        btServerThread = new BTServerThread();
        btServerThread.start();
    }

    @Override
    protected void onPause(){
        super.onPause();

        if(btServerThread != null){
            btServerThread.cancel();
            btServerThread = null;
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState){
        super.onSaveInstanceState(outState);
        outState.putString(Constants.STATE_TEMP, tempEditText.getText().toString());
    }

    public class BTServerThread extends Thread {

        BluetoothServerSocket bluetoothServerSocket;
        BluetoothSocket bluetoothSocket;
        InputStream inputStream;
        OutputStream outputStream;

        public void run() {

            byte[] incomingBuff = new byte[64];

            try {
                while (true) {

                    if (Thread.interrupted()) {
                        break;
                    }

                    try {

                        bluetoothServerSocket
                                = bluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord(
                                    Constants.BT_NAME,
                                    Constants.BT_UUID);

                        bluetoothSocket = bluetoothServerSocket.accept();
                        bluetoothServerSocket.close();
                        bluetoothServerSocket = null;

                        inputStream = bluetoothSocket.getInputStream();
                        outputStream = bluetoothSocket.getOutputStream();

                        while (true) {

                            if (Thread.interrupted()) {
                                break;
                            }

                            int incomingBytes = inputStream.read(incomingBuff);
                            byte[] buff = new byte[incomingBytes];
                            System.arraycopy(incomingBuff, 0, buff, 0, incomingBytes);
                            String cmd = new String(buff, StandardCharsets.UTF_8);

                            String resp = processCommand(cmd);
                            outputStream.write(resp.getBytes());
                        }

                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                    if (bluetoothSocket != null) {
                        try {
                            bluetoothSocket.close();
                            bluetoothSocket = null;
                        } catch (IOException e) {}
                    }

                    // Bluetooth connection broke. Start Over in a few seconds.
                    Thread.sleep(3 * 1000);
                }
            }
            catch(InterruptedException e){
                Log.d(TAG, "Cancelled ServerThread");
            }

            Log.d(TAG, "ServerThread exit");
        }

        public void cancel() {
            if (bluetoothServerSocket != null) {
                try {
                    bluetoothServerSocket.close();
                    bluetoothServerSocket = null;
                    super.interrupt();
                } catch (IOException e) {}
            }
        }

        protected String processCommand(String cmd){

            Log.d(TAG, "processCommand " + cmd);
            String resp = "OK";

            try {

                if (cmd.equals("GET:TEMP")) {
                    String s = ((BTServerApplication)MainActivity.this.getApplication()).getTempValue();
                    resp = (s == null) ? "n/a" : s;
                } else {
                    Log.d(TAG, "Unknown Command");
                }

            } catch (Exception e){
                Log.d(TAG, "Exception - processCommand " + e.getMessage());
            }

            return resp;
        }
    }
}

接続の受け付けを行うスレッドクラスは1つだけ定義しています。なるべく簡略化したかったので、 accept を呼び出した後、それが返ったらそのままデータ通信を同じスレッドで行うようにしています。

Constants.java

package com.keicode.android.testapps.bttest1s;

import java.util.UUID;

public interface Constants {
    public static final String BT_NAME = "BTTEST1";
    public static final UUID BT_UUID = UUID.fromString(
            "41eb5f39-6c3a-4067-8bb9-bad64e6e0908");
    public static final String STATE_TEMP = "STATE_TEMP";
}

UUID はここに書いてます。値は何でもいいですが、クライアント側も同じ UUID を使う必要があります。

BTServerApplication.java

package com.keicode.android.testapps.bttest1s;

import android.app.Application;

public class BTServerApplication extends Application {

    private String tempValue = "25";

    public synchronized String getTempValue(){
        return tempValue;
    }

    public synchronized void setTempValue(String s){
        tempValue =s;
    }
}

EditText の値はここに書き込んでます。UI スレッドとワーカースレッドの両方からアクセスされるので、synchronized としています。

クライアント・プログラムの実装

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.keicode.android.testapps.bttest1c">

    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.BLUETOOTH"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        >
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Bluetooth を利用するパーミッションを設定しています。ディスカバリは行わないので、LOCATION のパーミッションは設定していません。

res/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.keicode.android.testapps.bttest1c.MainActivity">

    <TextView
        android:id="@+id/btStatusTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Not connected" />

    <TextView
        android:id="@+id/tempTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="(unknown)"
        android:layout_below="@id/btStatusTextView"
        android:textSize="24sp"/>

</RelativeLayout>

状態を表示するための TextView と、サーバーから返された文字を表示するための TextView の計二つの TextView を並べています。

MainActivity.java

package com.keicode.android.testapps.bttest1c;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Set;

public class MainActivity extends AppCompatActivity {

    static final String TAG = "BTTEST1";
    BluetoothAdapter bluetoothAdapter;

    TextView btStatusTextView;
    TextView tempTextView;

    BTClientThread btClientThread;

    final Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {

            String s;

            switch(msg.what){
                case Constants.MESSAGE_BT:
                    s = (String) msg.obj;
                    if(s != null){
                        btStatusTextView.setText(s);
                    }
                    break;
                case Constants.MESSAGE_TEMP:
                    s = (String) msg.obj;
                    if(s != null){
                        tempTextView.setText(s);
                    }
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "onCreate");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Find Views
        btStatusTextView = (TextView) findViewById(R.id.btStatusTextView);
        tempTextView = (TextView) findViewById(R.id.tempTextView);

        if(savedInstanceState != null){
            String temp = savedInstanceState.getString(Constants.STATE_TEMP);
            tempTextView.setText(temp);
        }

        // Initialize Bluetooth
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if( bluetoothAdapter == null ){
            Log.d(TAG, "This device doesn't support Bluetooth.");
        }

    }

    @Override
    protected void onResume() {
        super.onResume();
        btClientThread = new BTClientThread();
        btClientThread.start();
    }

    @Override
    protected void onPause(){
        super.onPause();
        if(btClientThread != null){
            btClientThread.interrupt();
            btClientThread = null;
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(Constants.STATE_TEMP, tempTextView.getText().toString());
    }

    public class BTClientThread extends Thread {

        InputStream inputStream;
        OutputStream outputStrem;
        BluetoothSocket bluetoothSocket;

        public void run() {

            byte[] incomingBuff = new byte[64];

            BluetoothDevice bluetoothDevice = null;
            Set<BluetoothDevice> devices = bluetoothAdapter.getBondedDevices();
            for(BluetoothDevice device : devices){
                if(device.getName().equals(Constants.BT_DEVICE)) {
                    bluetoothDevice = device;
                    break;
                }
            }

            if(bluetoothDevice == null){
                Log.d(TAG, "No device found.");
                return;
            }

            try {

                bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(
                        Constants.BT_UUID);

                while(true) {

                    if(Thread.interrupted()){
                        break;
                    }

                    try {
                        bluetoothSocket.connect();

                        handler.obtainMessage(
                                Constants.MESSAGE_BT,
                                "CONNECTED " + bluetoothDevice.getName())
                                .sendToTarget();

                        inputStream = bluetoothSocket.getInputStream();
                        outputStrem = bluetoothSocket.getOutputStream();

                        while (true) {

                            if (Thread.interrupted()) {
                                break;
                            }

                            // Send Command
                            String command = "GET:TEMP";
                            outputStrem.write(command.getBytes());
                            // Read Response
                            int incomingBytes = inputStream.read(incomingBuff);
                            byte[] buff = new byte[incomingBytes];
                            System.arraycopy(incomingBuff, 0, buff, 0, incomingBytes);
                            String s = new String(buff, StandardCharsets.UTF_8);

                            // Show Result to UI
                            handler.obtainMessage(
                                    Constants.MESSAGE_TEMP,
                                    s)
                                    .sendToTarget();

                            // Update again in a few seconds
                            Thread.sleep(3000);
                        }

                    } catch (IOException e) {
                        // connect will throw IOException immediately
                        // when it's disconnected.
                        Log.d(TAG, e.getMessage());
                    }

                    handler.obtainMessage(
                            Constants.MESSAGE_BT,
                            "DISCONNECTED")
                            .sendToTarget();
                    
                    // Re-try after 3 sec
                    Thread.sleep(3 * 1000);
                }

            }catch (InterruptedException e){
                e.printStackTrace();
            }
            catch (IOException e) {
                e.printStackTrace();
            }

            if(bluetoothSocket != null){
                try {
                    bluetoothSocket.close();
                } catch (IOException e) {}
                bluetoothSocket = null;
            }

            handler.obtainMessage(
                    Constants.MESSAGE_BT,
                    "DISCONNECTED - Exit BTClientThread")
                    .sendToTarget();
        }
    }
}

こちらもワーカースレッド用に1クラス定義しているだけです。

例外が発生したらその状況を Handler を通して UI 側にフィードバックしています。

Constants.java

package com.keicode.android.testapps.bttest1c;

import java.util.UUID;

public interface Constants {
    public static final String BT_DEVICE = "Nexus 7";
    public static final UUID BT_UUID = UUID.fromString(
            "41eb5f39-6c3a-4067-8bb9-bad64e6e0908");

    public static final String STATE_TEMP = "STATE_TEMP";

    public static final int MESSAGE_BT = 0;
    public static final int MESSAGE_TEMP = 2;
}

UUID はここに記述しています。サーバー側の UUID と同じ値を設定します。

また、サーバーとなるデバイスは "Nexus 7" としてペアリングしているので、ここにデバイス名を書いています。Bonded リストから、 "Nexus 7" という名前の BluetoothDevice オブジェクトを取得したら接続に行くようになってます。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Android 開発入門