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 オブジェクトを取得したら接続に行くようになってます。