初めに
前回はArduinoとBluetooth送受信機(以下、BLEデバイスとする)の接続、受信した情報をデコードするところまでやりました。今回は前回の課題に挙がっていたBLEデバイスをコードでしか変更できない問題について解決していこうと思います。1
アプリ動作順序について
コードの説明の前に前提条件を理解しておきます。
前回ではアプリ起動時にプログラムされたBLEデバイスに接続する操作をしていました。今回はそのアクセス前にBLEを選択する動作を加えます。
Androidのアクティビティ実行動作順序(ライフサイクルイベント)は下のように規定されています。
画像引用元:知らずに作って大丈夫?Androidの基本的なライフサイクルイベント31選
前回作成したプログラムのアクティビティは上図のonCreate()
で全て動作していました。そこで、今回は下記のようにします。
-
onCreate()
:BLEデバイスの選択 -
onStart()
:選択されたBLEデバイスとの接続&その他全ての操作の実行
BLEデバイス選択用ソースコード作成
では結論として、初めにBLEデバイスの選択、接続、実行するための全てのソースコードを示します。内容は後から解説します。
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を押すと作成できます。
では、作成したファイルに以下のコードを書いてください。処理については後ほど説明します。
//動作環境に合わせて変更してください
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
//動作環境に合わせて変更してください
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
}
//動作環境に合わせて変更してください
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が作成されていると思います。
中身を下記のように変更してください。
<?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ファイルを新規作成して下記コードに書き変えてください。
<?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を認識させます。
<?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行分表示されています。
BLE接続するためには接続したいデバイスの行のCONNCETボタンを押してから、下部中央にあるCONNCET!ボタンを押します。2 BLEデバイスとの接続がされれば前回と同様にHello World!と書かれた画面に遷移して、Android Studio上で受信データが確認できるはずです。
ソースコードの解説
前回との差分 イメージ
初めに前回から追記した部分のイメージを簡単に説明します。
今回はスマホのペアリング設定済みのデバイスの中から接続先を選択する方式を取りたいと思います。3そこでBLEデバイスを選択する操作をMainActivityとは別のSelectActivityというActivityを実行することとしました。
文字の説明だけでは差分が分かりづらいと思いますので、下図で全体のイメージを可視化します。
前回のプログラムではMainActivity
のonStart()
で全て実行していましたが、それを分けている感じです。
MainActivity⇒SelectActivityへの遷移
では細かくコードの内容を見ていきましょう。
前回のコードの差分としてはまずMainActivity
のonCreate()
です。
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_device
はMainActivity
とSelectActivity
で接続する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デバイスの情報を先ほど定義したdede
とpusta
に格納します。
その後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を押すと下記の操作が実行されます。グローバル変数のstatus
にposition
変数、つまり行番号を代入します。
holder.sButton.setOnClickListener {
//Log.d(TAG,position.toString())
status = position
}
接続先決定とSelectActivtyの終了
SelectActivity
に戻り、最後のボタンの動作です。ボタンが押されると下記のsetOnClickListener
の中身が実行されます。内容としては前述したグローバル変数であるconn_device
にstatus
番目のBLEデバイスの情報が格納されます。つまり、選択したデバイスがconn_device
変数に格納されます。
そしてfinish()
でSelectActivity
を終了します。
button.setOnClickListener {
//intent.putExtra("po",status)
conn_device = dede[status]
finish()
}
終了後、MainActivity
のonStart()
に入ります。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)
}
レイアウトと変数の対応について
ここまで様々なレイアウトに関する変数を定義していましたが、どの変数がどのレイアウトに対応しているかが分かりづらいと思いましたので下図に示します。
まとめ
これで任意のBLEデバイスをアプリ上で選択して接続できます。今回の記事の目的としては単純に動かせるものを作ることでしたので、レイアウトについては参考資料を参考にしつつ、要件に応じて足したり引いたりしました。作成する際には皆さまもオリジナリティを出して修正・作成してみてください。
執筆する前はそこまで深い内容にならないだろうと思っていたのですが、3週間ほど時間がかかってしまいました。また、前回からだいぶ日が経ってしまったのでAndroid studioの環境も変わっており、そこの学習も大変でした。
次回は受信データを適用したレイアウトを作成していきます、、がちょっと別のネタを公開していきますのでだいぶ後になるかもしれません。気長に待っていただければ幸いです。
もし内容に関して不備、間違い、不適切な点ありましたらコメント欄でご連絡いただければ幸いです。まだまだ勉強中ですので至らない点多々あると思いますがよろしくお願いします!!
参考文献、サイトまとめ
-
【超初心者向け】Android入門 RecyclerView編
レイアウトのリスト作成とても参考になりました。リストの仕組みやレイアウトについてはこちらを参照いただければと思います。 -
【超初心者向け】Android入門 画面遷移編
こちらも別のActivityを起動させる、遷移させる機能についてとても参考になりました。上記同様にActivity関係の操作についてはこちらを参照いただければと思います。 - 【android】ActivityからActivityの画面遷移
- https://adsaria.hatenadiary.org/entry/20110428/1303966837
- 知らずに作って大丈夫?Androidの基本的なライフサイクルイベント31選