LoginSignup
1
1

Arduinoとスマホでお家をモニタPart2.5【Bletooth選択動作の作成】

Last updated at Posted at 2024-04-13

初めに

前回はArduinoとBluetooth送受信機(以下、BLEデバイスとする)の接続、受信した情報をデコードするところまでやりました。今回は前回の課題に挙がっていたBLEデバイスをコードでしか変更できない問題について解決していこうと思います。1

本記事で説明する内容の範囲は以下の通りです。
01.png

アプリ動作順序について

コードの説明の前に前提条件を理解しておきます。
前回ではアプリ起動時にプログラムされたBLEデバイスに接続する操作をしていました。今回はそのアクセス前にBLEを選択する動作を加えます。

Androidのアクティビティ実行動作順序(ライフサイクルイベント)は下のように規定されています。
image.png
画像引用元:知らずに作って大丈夫?Androidの基本的なライフサイクルイベント31選

前回作成したプログラムのアクティビティは上図のonCreate()で全て動作していました。そこで、今回は下記のようにします。

  • onCreate():BLEデバイスの選択
  • onStart():選択されたBLEデバイスとの接続&その他全ての操作の実行

BLEデバイス選択用ソースコード作成

では結論として、初めにBLEデバイスの選択、接続、実行するための全てのソースコードを示します。内容は後から解説します。

MainActivity.kt

MainActivity.kt
//動作環境に合わせてください
package com.mongodb.housemonitor

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket
import android.util.Log
import android.content.Intent
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream

const val TAG = "MainActivity"
//前回ペアリングしたRN-42のデバイス名をRNBT-○○○○で記述していますが今回は使用しないためコメントアウト
//const val TARGET_DEVICE_NAME = "RNBT-####"
const val REQUEST_ENABLE_BLUETOOTH = 1

class MainActivity : AppCompatActivity() {
    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
    private var connectedThread: ConnectedThread? = null
    private var connectThread: ConnectThread? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //レイアウトのactivity_mainはonStartで使用するためここでは記載しない
        //setContentView(R.layout.activity_main)
        val intent = Intent(this, SelectActivity::class.java)
        startActivity(intent)
    }
    override fun onStart() {
        super.onStart()
        setContentView(R.layout.activity_main)
        connectThread = ConnectThread(conn_device)
        connectThread?.start()
    }

    fun manageMyConnectedSocket(socket: BluetoothSocket) {
        connectedThread = ConnectedThread(socket)
        connectedThread?.start()
    }

    private inner class ConnectThread(device: BluetoothDevice?) : Thread() {
        private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
            device?.createInsecureRfcommSocketToServiceRecord(device.uuids[0].uuid)
        }

        public override fun run() {
            // Cancel discovery because it otherwise slows down the connection.
            bluetoothAdapter?.cancelDiscovery()
            if (mmSocket == null) {
                return
            }
            val socket = mmSocket
            socket ?: return
            socket.connect()
            // The connection attempt succeeded. Perform work associated with
            // the connection in a separate thread.
            manageMyConnectedSocket(socket)
        }

        // Closes the client socket and causes the thread to finish.
        fun cancel() {
            try {
                mmSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the client socket", e)
            }
        }
    }

    private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() {

        private val mmInStream: InputStream = mmSocket.inputStream
        private val mmOutStream: OutputStream = mmSocket.outputStream
        // 受け取る文字列のサイズを1にします。1バイトで受け取るためです。
        private val mmBuffer: ByteArray = ByteArray(1)
        //変換した文字列の格納変数
        var str2 = ""

        override fun run() {
            var numBytes: Int // bytes returned from read()
            Log.d(TAG, "connect start!")
            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                while(true){
                    numBytes = try {
                        mmInStream.read(mmBuffer)
                    } catch (e: IOException) {
                        Log.d(TAG, "Input stream was disconnected", e)
                        break
                    }
                    //改行した時
                    if(mmBuffer[0].toString()=="13") {
                        break
                    }
                    //,(カンマ)が送られた時
                    else if (mmBuffer[0].toString() == "44") {
                        break
                    }
                    //それ以外(数値や文字が送られてきたとき)
                    else {
                        //デコードに必要なキャラセットを作成
                        val charset = Charsets.UTF_8
                        //1バイトの情報を今まで受信してきた文字と組み合わせる(加算する)
                        //受信した1バイトの情報をUTF-8に変換 デコード操作
                        str2 += String(mmBuffer, charset)
                    }
                }
                // Logcatで表示
                Log.d(TAG,str2)
                // 表示した後に文字列変数を初期化
                str2=""
            }
        }


        // Call this method from the main activity to shut down the connection.
        fun cancel() {
            try {
                mmSocket.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
    }
}

SelectActivity.kt

BLEデバイスの検索と選択をするActivityです。初めにファイルを作成していきます。
プロジェクトのMainActivity.ktがあるディレクトリにSelectActivity.ktファイルを作成してください。作成方法はMainActivity.ktの入っているフォルダを右クリックしてNew>Activity>Galleryを選択して、Empty Views ActivityをクリックしてNEXTを押します。
出てきたウインドウのにActivity NameにSelectActivityと入力してFinishを押すと作成できます。
07.png
11.png

では、作成したファイルに以下のコードを書いてください。処理については後ほど説明します。

SelectActivity.kt
//動作環境に合わせて変更してください
package com.mongodb.housemonitor

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.Intent
import android.util.Log
import android.widget.Button
import androidx.core.view.get
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.util.*
//動作環境に合わせて変更してください
import com.mongodb.housemonitor.ViewAdapter
import kotlinx.android.synthetic.main.item_view.*

const val TAG_s = "selectActivity"
var conn_device: BluetoothDevice?=null
var status:Int=0

class SelectActivity : AppCompatActivity(){
    private lateinit var recyclerView: RecyclerView
    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()

    override fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_select)
        val button: Button = findViewById(R.id.button)
        var pusta: MutableList<String> = ArrayList()
        var dede: MutableList<BluetoothDevice> = ArrayList()
        //val intent = Intent(this, MainActivity::class.java)
        if (bluetoothAdapter==null){
            Log.d(TAG_s, "ble is not sup")
            return
        }
        val pairedDevices: Set<BluetoothDevice> = bluetoothAdapter.getBondedDevices()
        if (pairedDevices.size>0){
            for (device in  pairedDevices){
                val deb = device.name
                dede.add(device)
                pusta.add(deb)
            }
        }
        recyclerView=findViewById(R.id.recyclerView)
        recyclerView.adapter = ViewAdapter(pusta)
        recyclerView.layoutManager = LinearLayoutManager(this)
        button.setOnClickListener {
            //intent.putExtra("po",status)
            conn_device = dede[status]
            finish()
        }
    }
}

また、上記処理の中に必要なレイアウトを実行するために必要なクラスを作成していきます。SelectActivity.ktと同様の場所に下記二つのkotlinクラスを作成して、内容をそのさらに下に記されたコードに書き変えてください。

  • ViewAdapter.kt
  • ViewHolder.kt
ViewAdapter.kt
//動作環境に合わせて変更してください
package com.mongodb.housemonitor

import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import java.util.*

class ViewAdapter(val list: List<String>) : RecyclerView.Adapter<ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
        return ViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.positionText.text = position.toString()
        holder.titleText.text = list[position]
        holder.sButton.setOnClickListener {
            Log.d(TAG,position.toString())
            status = position
        }
    }
    override fun getItemCount(): Int = list.size
}
ViewHolder.kt
//動作環境に合わせて変更してください
package com.mongodb.housemonitor

import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val positionText: TextView = itemView.findViewById(R.id.position)
    val titleText: TextView = itemView.findViewById(R.id.title)
    val sButton: Button = itemView.findViewById(R.id.button)
}

上記2つのコードについては【超初心者向け】Android入門 RecyclerView編を参考にさせていただきました。

次に、レイアウトファイルも作成していきます。SelectActivity.ktが作成されるとresフォルダのlayoutフォルダにactivity_select.xmlが作成されていると思います。
中身を下記のように変更してください。

activity_select.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="400dp">
    </androidx.recyclerview.widget.RecyclerView>

    <Button
        android:id = "@+id/button"
        android:layout_width="120dp"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:text = "Connect!"
        />
</LinearLayout>

さらにactivity_select.xmlと同じ場所にxmlファイルを新規作成して下記コードに書き変えてください。

item_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="16dp">

    <TextView
        android:id="@+id/position"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:textColor="@android:color/black"
        android:textSize="22sp" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_marginStart="10dp"
        android:textColor="@android:color/black"
        android:textSize="22sp" />
    <Button
        android:id = "@+id/button"
        android:text="connect"
        android:layout_width="120dp"
        android:layout_height = "wrap_content"
        android:layout_gravity="end"
        />

</LinearLayout>

最後にmanifestファイルにSelectActivityを認識させます。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.mongodb.housemonitor">
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Housemonitor">
        <activity
            android:name=".SelectActivity"/>
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>
<activity
            android:name=".SelectActivity"/>

上記コードを追記しました。これを追記しないと動かないそうです。本件は下記のリンクを参考にさせていただきました。詳細は下記を参考にしてください。
参考> 【超初心者向け】Android入門 画面遷移編

動作結果

上記ソースコードを実行すると初めに次のような画面が表示されます。私の場合BLEデバイスを2つペアリング設定していましたので0、1と2行分表示されています。

05.png
BLE接続するためには接続したいデバイスの行のCONNCETボタンを押してから、下部中央にあるCONNCET!ボタンを押します。2 BLEデバイスとの接続がされれば前回と同様にHello World!と書かれた画面に遷移して、Android Studio上で受信データが確認できるはずです。

10.png

ソースコードの解説

前回との差分 イメージ

初めに前回から追記した部分のイメージを簡単に説明します。

今回はスマホのペアリング設定済みのデバイスの中から接続先を選択する方式を取りたいと思います。3そこでBLEデバイスを選択する操作をMainActivityとは別のSelectActivityというActivityを実行することとしました。

文字の説明だけでは差分が分かりづらいと思いますので、下図で全体のイメージを可視化します。
06.png
前回のプログラムではMainActivityonStart()で全て実行していましたが、それを分けている感じです。

MainActivity⇒SelectActivityへの遷移

では細かくコードの内容を見ていきましょう。
前回のコードの差分としてはまずMainActivityonCreate()です。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //レイアウトのactivity_mainはonStartで使用するためここでは記載しない
        //setContentView(R.layout.activity_main)
        val intent = Intent(this, SelectActivity::class.java)
        startActivity(intent)
    }

val intent=の行でselectActivityを定義して、次の行で実行しています。また、onCreate()ではactivity_main.xmlを実行しませんのでコメントアウトしています。

SelectActivity

ではSelectActivityの内容を説明していきます。初めに必要な変数を定義します。
下記のグローバル変数conn_deviceMainActivitySelectActivityで接続するBLEデバイスをやり取りするための変数です。この変数に接続したいBLEデバイスの情報を格納してMainActivityに渡して接続動作を行います。4
次のstatus変数はBLEデバイスが選択状態の時にその選択した行を記憶する変数です。

var conn_device: BluetoothDevice?=null
var status:Int=0

次にclass内部に入って、RecyclerView変数を定義します。本変数はスマホに登録されているBLEデバイスの情報をリストで表示するために必要なレイアウト用の変数です。次にBluetoothAdapter?変数についてはスマホがBluetoothを使用できるか確認するためのものです。
その後のonCreate()については使用するレイアウトファイル(activity_select.xml)を指定して必要な変数を用意しています。変数の意味は下記のとおりです。

  • button:BLEデバイスの選択状態の時に、選択されたデバイスに接続するトリガーとなるボタンです。
  • pusta:BLEデバイスの名前情報のみを格納する変数です。レイアウトで表示するときにこの名前を使用します。
  • dede:BLEデバイスのすべての情報を格納する変数です。

変数定義後のif文ではスマホがbluetoothを使用できるかを確認しています。使用できる場合は何もしませんが、使用不可の場合はアプリが終わりAndroid studio側でble is not supと表示されます。

class SelectActivity : AppCompatActivity(){
    private lateinit var recyclerView: RecyclerView
    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()

    override fun onCreate(savedInstanceState: Bundle?){
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_select)
        val button: Button = findViewById(R.id.button)
        var pusta: MutableList<String> = ArrayList()
        var dede: MutableList<BluetoothDevice> = ArrayList()
        //val intent = Intent(this, MainActivity::class.java)
        if (bluetoothAdapter==null){
            Log.d(TAG_s, "ble is not sup")
            return
        }

次のpairDevices変数からfor文ではスマホに登録されているBLEデバイスの情報を先ほど定義したdedepustaに格納します。
その後ViewAdapterクラスにpustaを渡し、リサイクラービューを表示させます。

val pairedDevices: Set<BluetoothDevice> = bluetoothAdapter.getBondedDevices()
        if (pairedDevices.size>0){
            for (device in  pairedDevices){
                val deb = device.name
                dede.add(device)
                pusta.add(deb)
            }
        }
        recyclerView=findViewById(R.id.recyclerView)
        recyclerView.adapter = ViewAdapter(pusta)
        recyclerView.layoutManager = LinearLayoutManager(this)

ViewAdapterとViewHolder

ViewAdapterでは初めにViewHolderクラスに表の一行分のレイアウトを作成させます。ViewHolderの中身は後述します。
ViewHolderのレイアウトをスマホがペアリング設定しているデバイス分作り出して表にします。レイアウトの設定はitem_view.xmlで設定しています。

holder.sButton.setOnClickListenerは各行(各BLEデバイス)にあるボタンが押されたときに実行される動作です。押されるとSelectActivity上で宣言したグローバル変数のstatusにデバイスを示す数字(行数)が格納されます。

class ViewAdapter(val list: List<String>) : RecyclerView.Adapter<ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
        return ViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.positionText.text = position.toString()
        holder.titleText.text = list[position]
        holder.sButton.setOnClickListener {
            //Log.d(TAG,position.toString())
            status = position
        }
    }
    override fun getItemCount(): Int = list.size
}

ViewHolderは前述したようにリストの一行分のレイアウトを表示するコードです。各変数については以下の通りです。

  • positionText: 行数(0,1,2,3,,,)を示します。
  • titleText: MainActivity上で取得したBLEデバイスの名前(pasuta変数)を元に表示します。
  • sButton: 各BLEデバイス(行)に設定されるボタンです。このボタンを押すことで同じ行にあるBLEデバイスが選択された状態となります。選択状態でMainActivityのボタンを押すことで接続動作を開始できます(後述)。
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val positionText: TextView = itemView.findViewById(R.id.position)
    val titleText: TextView = itemView.findViewById(R.id.title)
    val sButton: Button = itemView.findViewById(R.id.button)
}

次にonBindViewHolderの中身ですが先ほど定義したsButtonを押すと下記の操作が実行されます。グローバル変数のstatusposition変数、つまり行番号を代入します。

        holder.sButton.setOnClickListener {
            //Log.d(TAG,position.toString())
            status = position
        }

接続先決定とSelectActivtyの終了

SelectActivityに戻り、最後のボタンの動作です。ボタンが押されると下記のsetOnClickListenerの中身が実行されます。内容としては前述したグローバル変数であるconn_devicestatus番目のBLEデバイスの情報が格納されます。つまり、選択したデバイスがconn_device変数に格納されます。
そしてfinish()SelectActivityを終了します。

button.setOnClickListener {
            //intent.putExtra("po",status)
            conn_device = dede[status]
            finish()
        }

終了後、MainActivityonStart()に入ります。onStart()ではMainActivity用のレイアウトを実行します。次にconnectThread(device)SelectActivityで選択したBLEデバイスを引数に入れて接続作業に入ります。ここから先は前回の記事の内容と同じですので説明は省略します。

override fun onStart() {
        super.onStart()
        setContentView(R.layout.activity_main)
        connectThread = ConnectThread(conn_device)
        connectThread?.start()
    }

MainActivityの一部変更点

MainActivityの一部前回と異なる点を下に示しておきます。SelectActivity上でBLEデバイスの変数である conn_device: BluetoothDevice?を宣言しましたが、前回とは違い宣言時点で接続先のデバイスが決定していないため方に?があります。この変更に合わせて下記の部分で?を追記しています。

    private inner class ConnectThread(device: BluetoothDevice?)
    private inner class ConnectThread(device: BluetoothDevice?) : Thread() {
        private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
            device?.createInsecureRfcommSocketToServiceRecord(device.uuids[0].uuid)
        }

レイアウトと変数の対応について

ここまで様々なレイアウトに関する変数を定義していましたが、どの変数がどのレイアウトに対応しているかが分かりづらいと思いましたので下図に示します。
12.png

まとめ

これで任意のBLEデバイスをアプリ上で選択して接続できます。今回の記事の目的としては単純に動かせるものを作ることでしたので、レイアウトについては参考資料を参考にしつつ、要件に応じて足したり引いたりしました。作成する際には皆さまもオリジナリティを出して修正・作成してみてください。

執筆する前はそこまで深い内容にならないだろうと思っていたのですが、3週間ほど時間がかかってしまいました。また、前回からだいぶ日が経ってしまったのでAndroid studioの環境も変わっており、そこの学習も大変でした。

次回は受信データを適用したレイアウトを作成していきます、、がちょっと別のネタを公開していきますのでだいぶ後になるかもしれません。気長に待っていただければ幸いです。

もし内容に関して不備、間違い、不適切な点ありましたらコメント欄でご連絡いただければ幸いです。まだまだ勉強中ですので至らない点多々あると思いますがよろしくお願いします!!

参考文献、サイトまとめ

IoTシリーズ一覧

  1. 本記事の目的はこのシリーズの目的とは少しずれますし内容も前回や前々回と比較して薄くなっていますのでPart2.5としています。

  2. ボタンの表記の仕方が分かりづらいですね(笑)。必要に応じてFixしてみてください。

  3. 前回の記事ではRN-42をペアリング設定しました。そのため、今回のケースではペアリング設定済みのデバイスから選択する方式としました。

  4. グローバル変数でActivity間のやり取りをするのはあまり推奨しない場合があります。今回は難しいことを気にせずに単純に動かすため、一番Activity間の変数のやり取りが簡単な方法を選びました。必要に応じてFixしてください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1