PR

【Kotlin】AndroidアプリにBluetooth通信機能を実装する

Androidアプリ

スマホアプリで組み込み機器を制御したいです…

こうした悩みを解決します。

一回できれば何てことないですが、まあ動かず苦労します、ハードル高かった…

立プロ

新卒でメーカーに入り、10年間組み込みの現場で設計を行う。
今は個人事業主として自作の組み込み機器開発や、エージェント様に紹介いただき業務委託を行っています。
C,C#,JavaScript, Vue, PHP, VBA, GAS, Kotlinなど、扱う言語が増えゆく日々。

立プロをフォローする

はじめに

Android公式より、Bluetooth通信機能の実装方法について記載があります。

まずはこちらを参照ください。

Bluetooth の概要  |  Android デベロッパー  |  Android Developers
...

公式ぺージと照らし合わせながら実装していきます。

だいぶ長い作業になりますが、がんばって実装していきましょう。
(私は上記公式ぺージを見ながら実装を始めて丸3週間ほどかかりました)

Bluetooth接続の全体イメージ

私の理解では下図の構成でBluetooth通信を実行しています。
(間違っていたら問合せよりご教示いただけますと幸いです!)

↓ざっと口頭で説明した動画。一発撮りなので聞き苦しいかと思います、すみません。

↑できる限り見て!

Bluetoothパーミッション

アプリでBluetoothを使うために、マニフェストファイルにパーミッションを追加します。

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

これは公式のとおり、manifestファイルにコピペすればよいので問題ないでしょう。

プロファイルの使用

これはヘッドセットとかヘルス機器との接続に使うプロファイルなので、飛ばします。

Bluetooth使用の下準備

検出や接続、通信など一連のBluetooth処理はBluetoothAdapterを介して行われます。

スマホの中にBluetoothモジュールがあって、BluetoothAdapterを介して連携していると考えるとよいです。

BluetoothAdapterの取得

class MainActivity : AppCompatActivity() {
    val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(
)
    override fun onCreate(savedInstanceState: Bundle?) {
        ~~~~
    }

ここで定義したBluetoothAdapterはBluetooth通信スレッドを開始するまでずっと使い続けます!

bluetoothAdapterはBluetoothAdapter型だろう、と。

getDefaultAdapter関数からBluetoothAdapterを取得することができます、もしスマホ自体にBluetooth機能がなければnullが返ります。

変数の型に、nullの可能性があるよ、という意味の”?”がついていますが、上述の仕様のためです。

以下にサンプルコードを示します。

if (bluetoothAdapter == null) {
    Toast.makeText(applicationContext, "本体にBluetoothが存在しません", Toast.LENGTH_LONG).show()
    return
}

サンプルコードではアダプタが取得できない(=null)場合はトーストで通知するようにしています。

Bluetooth有効化

スマホがBluetoothを有効にしているか確認します。

もし無効になっている場合は、有効にするようリクエストするとよいとのこと。

ACTION_REQUEST_ENABLEで指定したインテント(=Yes/Noの選択肢をもつポップアップ)を出して、ユーザーの選択した結果をstartActivityForResultで受けます。

この時、リクエストコード(下記サンプルだとBT_ONOFF_CONF、事前に値を定義している)を指定して結果を渡します。

if (bluetoothAdapter.isEnabled == false) {
    val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)//bluetoothアダプタのリクエスト許可を行うインテントを作成

    //もし自己確認でBluetooth_connectのパーミッションが無かったら
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(arrayOf(Manifest.permission.BLUETOOTH_CONNECT),PERMISSION_BLUETOOTH_CONNECT_CODE) //リクエストパーミッションにbluetooth_connectを追加
    }
    startActivityForResult(enableBtIntent, BT_ONOFF_CONF) //戻り値のあるアクティビティとしてインテントを実行
}

//リクエストパーミッションの結果を処理
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        when (requestCode) {
            PERMISSION_BLUETOOTH_CONNECT_CODE -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    // パーミッションが許可された場合の処理を実行する
                    Toast.makeText(applicationContext, "許可しました", Toast.LENGTH_LONG).show()
                } else {
                    // パーミッションが拒否された場合の処理を実行する
                    Toast.makeText(applicationContext, "拒否しました", Toast.LENGTH_LONG).show()
                }
                return
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    }

//アクティビティの結果を表示
@RequiresApi(Build.VERSION_CODES.M)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    when(requestCode) {
        BT_ONOFF_CONF -> {
            when (resultCode) {
                Activity.RESULT_OK -> {
                        Toast.makeText(applicationContext, "許可しました", Toast.LENGTH_LONG).show()
                }
                Activity.RESULT_CANCELED -> { Toast.makeText(applicationContext, "未許可の場合、アプリは機能しません", Toast.LENGTH_LONG).show() }
            }
        }
    }
}

自己確認の部分は公式には記載無く、Androidstudio側で最近追加となったと思われます。

ペア設定済みの端末の問い合わせ

スマホと既にペアリングしている機器を問い合わせます。

bondedDevicesで問い合わせて、デバイスの名称とMACアドレスを取得します。

スマホから機器に接続する時用いるのはMACアドレスだけです。

デバイス名称を指定して対応するMACアドレスを記憶させることが多いです。

ペア設定済み端末をリストに格納してユーザーに接続する機器を選ばせることもできますが、本記事では最短でBluetooth通信をすることを目的としているのでリストへの格納は省略します。

val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
        pairedDevices?.forEach { item -> //ペア設定を取得
            if(item.name == "tatepro"){
                MACaddr = item.address
            }
        }
}

デバイス名称tateproのMACアドレスをMACaddr変数に格納しています。

端末の検出

これはスマホと接続する機器のペアリング設定ですが、アプリ側でやるよりスマホ本体の設定画面でさせた方がよいかと思いますので、割愛します。

ここまでをいったん整理しますと、

・アプリがスマホのBluetoothを使うためのアダプタを作りました

・スマホ自体にBluetooth機能があるか確認しました

・Bluetooth機能があって、無効になっている場合は有効にするか確認するようにしました

ここまでがアプリ側設定の話で、ここから機器と接続する部分になります。

端末の接続(=Bluetooth通信スレッドの生成)

デバイスとソケットを用いたBluetooth通信処理、UIスレッドとLooper/Handlerを用いたスレッド間通信処理を行うためのBluetooth通信スレッドを作成します。

スマホアプリはクライアント側です。

クライアントというのは、MACアドレスを指定して作ったBluetoothオブジェクトからソケットを作って接続する方です。

余談ですが、サーバ側というのはソケット待ちのオープン状態にしている方です。

Bluetooth通信スレッドの作成

このスレッドの立て方が公式の説明だとまあ何と分かりづらいこと…。

手順としては、まず上記の手順で取得したMACアドレスを引数としてgetRemoteDeviceメソッドでインスタンスを生成します。

そしてこのインスタンスを引数としたBluetooth通信スレッド(ConnectThread)を作成し、実行します。

start関数によって、Bluetooth通信スレッドのrun関数が実行されます。

device = bluetoothAdapter.getRemoteDevice(MACaddr) //BluetoothDeviceインスタンス(=接続するリモート端末のこと)の生成
connThread = ConnectThread(device)
connThread.start()

デバイスとの通信確立

アプリとデバイス間はソケットを使って通信します。

createRfcommSocketToServiceRecord関数は、Bluetoothインスタンスに対してRFCOMM(Radio Frequency Communication)チャネルを開いて、UUIDに対応するサービスに接続するBluetoothSocketを作成します。

UUIDは固有値なので、何も考えずコピペでよいです。

ソケットには受信用(inputStream)と送信用(outputStream)があるので、それも定義しておきます。

またバッファは受信データを格納する配列です。

あとハンドラはBluetooth通信だからというわけではないですが、スレッド間通信(今回でいうとUIスレッド)でデータをやり取りするために必要な出入り口、なのでこれも作っておきます。

上で述べたUIスレッドのスレッドstart関数を使うことでrunが動きます この中でConnect関数にてソケットの接続が行われます。 この接続処理がブロッキングコールと言ってアプリ内の処理を占有してしまうので、別のスレッドを立てて実行しなければならないというわけです。

接続処理は 12秒間行われ、この間に接続が成功すればmanageMyConnectedSocket関数に遷移、失敗すればtry-catchのioxceptionの処理に移ります。

デバイスとの送受信処理は、manageMyConnectedSocket関数内で行われます。これは後述。

    inner class ConnectThread(device: BluetoothDevice) : Thread() {
        var MY_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
        val mmSocket = device.createRfcommSocketToServiceRecord(MY_UUID)
        val mmInStream: InputStream = mmSocket.inputStream
        val mmOutStream: OutputStream = mmSocket.outputStream
        var mmBuffer: ByteArray = ByteArray(128) // mmBuffer store for the stream
        var mHandler: Handler = mainHandler

        override fun run() {    //ConnectThread(device).start()で呼ばれる
            mmSocket?.use { socket ->
                try{ socket.connect() }//ソケットを通してリモートデバイスに接続
                catch (e: IOException) {
                    return  //接続に失敗したので戻る
                }
                manageMyConnectedSocket() //接続の試行に成功、通信に関連する作業を実行
            }
        }
        // クライアントソケットを閉じ、スレッドを終了させる ConnectThread(device).cancel()で呼ばれる
        fun cancel() {

        }

        //データ転送用のスレッドを開始
        fun manageMyConnectedSocket() {
           /*後述*/
        }
    }

接続の管理(=デバイスとの送受信処理)

manageMyConnectedSocket関数に送受信処理を書いていきます。

公式の説明だとmanageMyConnectedSocketの詳細は「接続の管理」で説明と書いてますが、参照するとMyBluetoothServiceクラスの中にソケットを引数としたConnectedThreadスレッドを作成して通信処理をさせているんですね、manageMyConnectedSocketとはいかに…。

manageMyConnectedSocketについては私が作成したコードを参照していただければと思いますが、大きくはConnectedThreadと大差ないです。

whileで送信と受信の処理を行い、ハンドラで受信データをUIスレッドに渡すというものです。

まとめ

このイメージで動作してる…はず。

コメント

  1. 匿名 より:

    大変参考になりました。ありがとうございます。
    manageMyConnectedSocket内の処理について「私が作成したコードを参照」とありますが、どこから取得できますでしょうか。

タイトルとURLをコピーしました