AndroidスマホとRaspberry Pi Pico WをBluetoothシリアル通信接続してステッピングモータを制御する実験(Kotlinでスマホアプリ作成)


AndroidスマホとRaspberry Pi Pico WをBluetoothシリアル通信で接続してステッピングモータを制御する実験を紹介します。スマホ側はAndroid Studioを使用してKotlin言語でステッピングモータを操作するアプリを作成しました。Raspberry Pi Pico W側のプログラミングツールはArduino IDEを使用しました。

Bluetoothには古くから存在するBluetoothクラシックと、後から開発され消費電力を大幅に削減出来るBluetooth Low Energyがあります。それぞれ互換性はありません。Raspberry Pi Pico Wは両方の規格に対応しています。今回はBluetoothクラシックのシリアルポートプロファイルを使用しました。

以前の記事でパソコンのCOMポートを使用してBluetoothシリアル通信でRaspberry Pi Pico Wを接続してステッピングモータの動作を制御する実験を紹介しました。パソコンはCOMポートを使用することで簡単にBluetoothシリアル通信が行えましたが、スマホの場合COMポートの機能がないのでアプリ製作は厄介です。(パソコンとRaspberry Pi Pico WをBluetoothシリアル通信接続する記事はこちら

1.動作動画

今回作成した実験装置を動作させた動画です。スマホとRaspberry Pi Pico WをBluetooth接続した後に、スマホ画面の「CW」ボタンを押すとステッピングモータがCW方向に回転します。また「CCW」ボタンを押すとステッピングモータがCCW方向に回転します。

2.実験装置

ステッピングモータは28BYJ-48とそのドライバーボードのセットを使用しました。このステッピングモータとドライバーボードは複数のネット通販から購入することが出来ます。
コントローラとしてはRaspberry Pi Pico Wを使用して実験装置を組みました。(ステッピングモータ駆動の詳細に関してはこちらを参照して下さい)

28BYJ-48ステッピングモータとドライバ
28BYJ-48ステッピングモータ裏面
実験装置

今回使用したステッピングモータの主な仕様は表1の通りです。このモータは1/64のギアで減速されていて、1-2相励磁の場合にステップ角は5.625°/64なので4096ステップでモータ軸が1回転します。最大自起動周波数が500ppsなので、500パルス/sec以下で起動する必要があります。起動後は徐々に加速を行った場合に、最大で1000パルス/secまで仕様上は可能と思われます。
ただし、Arduino IDEのStepperライブラリーのstep()関数を使用した場合の励磁方式は2相励磁のようです。そのため2048ステップでモータ1回転となります。

項目仕様
相数4相
励磁方式1-2相励磁方式
ステップ角5.625°/64   (減速比1/64)
電圧5VDC
相抵抗22Ω ± 7% 25°C
最大応答周波数1000pps
最大自起動周波数500pps
引き込みトルク800gf.cm / 5VDC 400pps
表1.28BYJ-48ステッピングモータの主な仕様

3.実験装置回路図

図1に実験装置の回路図を示します。1個のステッピングモータをRaspberry Pi Pico Wに接続しています。

28BYJ-48ステッピングモータのドライバーボードはテキサスインスツルメンツのULN2003ANと言うダーリントントランジスタアレイが使用されています。ドライバーボード自体の資料が無かったので、テスタと基板のパターンを見て回路を調べました。

ステッピングモータのドライバーボードの入力信号IN1~IN4をRaspberry Pi Pico WのGP0~GP3に接続しました。

Bluetoothシリアル通信を使用してスマホと接続します。

図1.実験装置の回路図

4.スマホの設定と接続相手のBluetoothアドレスの調査

Bluetoothクラシックの機能としては、周辺デバイスの検索やペアリングなどの機能があります。今回これらの機能はスマホが持っている機能を使用してBluetooth接続のみを行うアプリとします。

今回作成したアプリではRaspberry Pi Pico WとBluetoothシリアル通信接続する場合、Raspberry Pi Pico WのBluetoothアドレスが必要です。

Raspberry Pi Pico WのBluetoothアドレスをスマホ側から知る方法を以下に記載します。

4-1.Raspberry Pi Pico WのBluetoothアドレスをスマホから調べる方法

①Raspberry Pi Pico Wに6項で記載したプログラムを入れて電源を投入してAndroidスマホの近くに置きます。

②スマホアプリの「設定」を開いて、「接続設定」をタップします。

③「接続の詳細設定」をタップします。

④「Bluetooth」をタップします。

⑤「Bluetoothを使用」にして「新しいデバイスとペア設定」をタップすると周辺のBluetoothデバイスの検索が開始されます。

⑥Raspberry Pi Pico Wが検出されると、「PicoW Serial ——」と表示が出ます。ここでSerialの後に表示される文字列(ここでは28:CD:C1:0E:32:6D)はRaspberry Pi Pico WのBluetoothアドレスです。今回作成するスマホアプリでは、このアドレスを使用してBluetooth接続します。

➆PicoW Serial—-と表示された部分をタップすると、下記のようにペア設定要求画面が表示されます。「ペア設定する」をタップするとスマホとRaspberry Pi Pico Wがペア設定されて今後の接続が素早く出来るようになります。

表示される「Bluetoothペア設定コード」はデータ暗号化のためのキーでスマホとRaspberry Pi Pico Wとの間でこの数値が交換されています。Raspberry Pi Pico W側はSerialBTライブラリ内で数値の処理が行われているようで特に操作する必要はありません。

5.スマホ側アプリに関して

スマホ側のアプリはAndroid IDEを使用してKotlin言語で作成しました。今回使用したスマホのAndroidのバージョンは14です。
アプリはステッピングモータを手動操作するものです。以下で作成アプリに関して説明します。

5-1.ManifestファイルにPermissonを追加する

Bluetooth機能を使用するアプリはmanifestsにBluetooth関係のPermissonを設定する必要があります。
app→manifests→AndroidManifest.xmlに以下の様なBluetoothに関するPermissonを追加します。(5行目~15行目)
今回のアプリでは不要なものでもBluetoothアプリに必要なPermissonを全て追加しました。

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

    <uses-feature android:name="android.hardware.bluetooth"/>
    <!--Android12未満に必要なパーミッション-->
    <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
    <!--Android12以上に必要なパーミッション-->
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    <!--Android12未満、Android12以上両方に必要なパーミッション-->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"

5-2.文字列定義strings.xmlコード

今回作成したアプリの文字列を定義するapp→res→values→strings.xmlのコードを以下に示します。

resources>
    <string name="app_name">Bluetooth JOG テスト</string>
    <string name="cw_button">CW</string>
    <string name="ccw_button">CCW</string>
    <string name="connect_button">接続</string>
    <string name="disconnect_button">切断</string>
    <string name="bt_adress">Blutoothアドレス</string>
</resources>

5-3.アプリ画面のレイアウトの作成

アプリ画面のレイアウトは押し釦スイッチ4個とEditTextを1個とTextViewを2個配置しました。以下に画面レイアウトと各Viewのidと役割に関して記します。

名称種類id役割
CWボタンButtoncwButtonモータをCW方向に回転させるボタン
CCWボタンButtonccwButtonモータをCCW方向に回転させるボタン
接続ボタンButtonconnectButtonBluetooth接続を行うボタン
切断ボタンButtondisconnectButtonBluetoothを切断するボタン
Bluetoothアドレス入力EditTextEditTextbtAddEditText接続相手(Pico W)のBluetoothアドレスを入力する
現在位置表示TextViewTextViewposTextViewモータの回転位置を表示する
“Blutoothアドレス” TextViewTextViewbtAddLabelTextView“Bluetoothアドレス”と言う文字列を表示する
表2.各Viewの役割とid

画面レイアウトに対応したXMLのコード(app→res→layout→activity_main.xml)を以下に示します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <Button
        android:id="@+id/cwButton"
        android:layout_width="147dp"
        android:layout_height="74dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="24dp"
        android:text="@string/cw_button"
        android:textSize="34sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/posTextView" />

    <Button
        android:id="@+id/ccwButton"
        android:layout_width="147dp"
        android:layout_height="74dp"
        android:layout_marginTop="24dp"
        android:layout_marginEnd="24dp"
        android:text="@string/ccw_button"
        android:textSize="34sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/posTextView" />

    <TextView
        android:id="@+id/posTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginTop="32dp"
        android:text="X 0000"
        android:textSize="48sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/btAddLabelTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="132dp"
        android:text="@string/bt_adress"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/cwButton" />

    <EditText
        android:id="@+id/btAddEditText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="121dp"
        android:ems="10"
        android:inputType="text"
        android:text="00:00:00:00:0000"
        app:layout_constraintStart_toEndOf="@+id/btAddLabelTextView"
        app:layout_constraintTop_toBottomOf="@+id/cwButton" />

    <Button
        android:id="@+id/connectButton"
        android:layout_width="147dp"
        android:layout_height="74dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="8dp"
        android:onClick="connectBtOnClick"
        android:text="@string/connect_button"
        android:textSize="34sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btAddEditText" />

    <Button
        android:id="@+id/disconnectButton"
        android:layout_width="147dp"
        android:layout_height="74dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="24dp"
        android:onClick="disconnectBtOnClick"
        android:text="@string/disconnect_button"
        android:textSize="34sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btAddEditText" />

</androidx.constraintlayout.widget.ConstraintLayout>

5-4.Blutoothシリアル通信のKotlinプログラムコードの要素

今回作成したアプリのKotlinプログラム(MainActivity.kt)のプログラムコードからBluetoothシリアル通信に関する主要要素の部分を抜き出して説明します。

①BluetoothAdapterの取得

Bluetooth通信を行うためには最初にBluetoothAdapterを取得しないとなりません。その部分のプログラムコードを以下に示します。

    override fun onCreate(savedInstanceState: Bundle?) {
              .
              .省略
              .
        //BluetoothAdapterの取得
        val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
        if (bluetoothAdapter == null){
            Toast.makeText(this@MainActivity, "Bluetooth機能ありません", Toast.LENGTH_LONG).show()
        }else{
            Toast.makeText(this@MainActivity, "Bluetooth OK", Toast.LENGTH_LONG).show()
        }
              .
              .省略
              .
    }

【説明】
6行目:
まず最初にBluetoothAdapter.getDefaultAdapter()でBluetoothAdapterを取得します。

7行目:
取得したBlutoothAdapterがnullの場合は、そのスマホはBluetooth機能が使用できません。

②Bluetooth接続のユーザ承認の要求

Bluetooth接続を行うためには、5-1項で示した通りmanifestsのAndroidManifest.xmlで最低でも
<uses-permission android:name=”android.permission.BLUETOOTH_CONNECT”/>
の記載が必要です。
但しAndroid12 (API31)以降のバージョンでは、manifestsで宣言しあってもアプリ側でBluetooth接続のユーザ承認を要求をしないとアプリが異常停止してしまいます。
Bluetooth接続の承認をユーザ側に要求する部分のプログラムコードを以下に示します。

    override fun onCreate(savedInstanceState: Bundle?) {

              ・省略

        //-----Bluetooth接続のパミッションを要求する----
        //API30より大きいバージョンではパミッション要求
        if (Build.VERSION.SDK_INT > 30) {
            if (ContextCompat.checkSelfPermission(
                    this,
                    android.Manifest.permission.BLUETOOTH_CONNECT
                )
                != PackageManager.PERMISSION_GRANTED
            ) {
                requestPermissionLauncher.launch(android.Manifest.permission.BLUETOOTH_CONNECT)
            }

            //Bluetooth接続パミッション許可待ち処理
            var No: Int = 0
            while (true) {
                if (ContextCompat.checkSelfPermission(
                        this,
                        android.Manifest.permission.BLUETOOTH_CONNECT
                    ) == PackageManager.PERMISSION_GRANTED
                ) {
                    break
                }

                No++
                if (No > 500) {      //5秒経過
                    Toast.makeText(this@MainActivity, "Bluetooth接続が許可されません", Toast.LENGTH_LONG)
                        .show()
                    break;
                }
                Thread.sleep(10)   //10msec待ち
            }
        }

              ・省略

    }
    
    
    //Bluetoothパミッション要求に対するコールバック
    private val requestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
            if (isGranted) {
                Toast.makeText(this@MainActivity, "パミッションが承諾されました", Toast.LENGTH_LONG).show()
            } else {
                Toast.makeText(this@MainActivity, "パミッションが拒否されました", Toast.LENGTH_LONG).show()
            }
        }

【説明】
7行目~15行目:
APIバージョンが30より大きい場合に、Bluetooth接続のパミッションが承認されていない時、BLUETOOTH_CONNECTのユーザ承認を要求します。

18行目~35行目:
Bluetoothの接続をユーザーが承認するまで待ちます。

③Bluetoothが有効となっているかの確認

スマホがBluetooth機能を持っていても、Bluetoothが有効に設定されていなければ使用出来ないのでその確認をします。以下はその部分のプログラムコードです。

    override fun onCreate(savedInstanceState: Bundle?) {
              .
              .省略
              .
                        
        //Bluetooth有効確認
        if (bluetoothAdapter?.isEnabled == true){
            Toast.makeText(this@MainActivity, "Bluetooth有効", Toast.LENGTH_LONG).show()
        }else{
            Toast.makeText(this@MainActivity, "Bluetooth無効となっています", Toast.LENGTH_LONG).show()
            //Bluetooth無効となっている場合は、Bluetooth有効化要求をする
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            btActivityResultLuncher.launch(enableBtIntent)
        }
              .
              .省略
              .
    }
        
        
    //Blutooth有効化要求に対するコールバック
    private val btActivityResultLuncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ){
        if (it.resultCode == RESULT_OK){
            runOnUiThread {
                Toast.makeText(this@MainActivity, "Bluetoothが有効化されました", Toast.LENGTH_LONG).show()
            }
        }
    }

【説明】
7行目:
Bluetoothが有効になっているかの確認をします。

12,13行目:
Bluetoothが無効になっている場合は有効にするように促す通知を表示します。

④Bluetoothの接続

Bluetoothデバイス同士の接続は、既にボンディングした同士で接続したり、接続相手のBluetoothアドレスを直接指定して接続したりする方法があるようですが、今回作成したプログラムでは接続相手のBluetoothアドレスを直接指定して行います。以下はその部分のソースコードです。
ここでは仮に接続相手のアドレスを28:CD:C1:0E:32:6Dとしたプログラム例です。実際のプログラムはEditTextでアドレスを入力するように作成しました。

//接続先のBluetoothアドレス指定してBluetoothDeviceを取得する
val device: BluetoothDevice? = bluetoothAdapter?.getRemoteDevice("28:CD:C1:0E:32:6D")

//接続スレッドスタート
connectThread = ConnectThread(device!!)
connectThread.start()

【説明】
2行目:
接続相手のBluetoothアドレスを指定してBluetooth接続します。接続相手のBluetoothアドレスを調べる方法は4-1項を参照して下さい。ここではBlutoothアドレスが28:CD:C1:0E:32:6Dの場合の例を示します。bluetoothAdapter?.getRemoteDevice(“28:CD:C1:0E:32:6D”)でBluetoothDeviceを取得します。

6行目:
Bluetooth接続にはある程度の時間がかかるので、「接続」ボタンを押した時のリスナープログラム内で行うと動作が停止してしまうので、新たにスレッドを起動して行います。

⑤Bluetooth接続スレッド

Bluetooth接続にはある程度の時間がかかるので、「接続」ボタンを押した時のリスナープログラム内で行うと動作が停止してしまうので、新たにスレッドを起動して行います。以下にそのスレッドのソースコードを示します。

Bluetoothの接続はBluetoothソケットを作成してソケットの接続で行います。送受信はそのソケットストリームに対するread(),write()メッソドを使用して行います。

//--------Bluetooth接続スレッド------------------
private inner class ConnectThread(val device: BluetoothDevice) : Thread() {

  //BluetoothSocket
  private lateinit var mmSocket: BluetoothSocket

  //シリアルポートプロファイルのUUID
  private val MY_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");

  public override fun run() {
    //Bluetoothソケットの取得
    mmSocket = this.device.createRfcommSocketToServiceRecord(MY_UUID)

    try {
      //Bluetoothソケット接続
      mmSocket.connect()
    } catch (e: IOException) {
      runOnUiThread {
        Toast.makeText(this@MainActivity, "Bluetooth接続できません", Toast.LENGTH_LONG).show()
      }
      return
    }
    
    //接続成功表示
    runOnUiThread {
      Toast.makeText(this@MainActivity, "Bluetooth接続しました", Toast.LENGTH_LONG).show()
    }

    //受信処理等の実施
    rcvPosData() //現在位置データ受信処理
  }
}

【説明】
8行目:
Bluetoothソケットを作成するためには、UUIDを指定する必要があります。UUIDはBluetoothのプロファイルに固有の数値を指定して得る必要があります。その数値はシリアルポートプロファイルの場合
00001101-0000-1000-8000-00805f9b34fb
です。UUID.formString()メソッドの引数としてString型で与えます。

12行目:
BluetoothDeviceに対してcreateRfcommSocketToServiceRecord()メソッドを実行することで、Bluetoothソケットが取得されます。

16行目:
Bluetoothソケットのconnect()メソッドでBluetooth接続が行われます。

30行目:
rcvPosData()は受信処理のメソッドです。whileループ等を使用してメソッド内部で繰り返し受信処理を行います。

⑥受信データの読み込み

データ受信は、BluetoothSocketの入力ストリームを取得して、そのストリームに対するread()メソッドで行います。
以下コードでは最大で1024バイトの受信データをバイトアレー rcvBufferに読出します。メソッドのリターン値numBytesは受信バイト数です。
read()メソッドはデータ受信するまで抜け出ません。(6行目)

val RCV_BUF_SIZE: Int = 1024    //受信バッファサイズ
val inStream: InputStream = mmSocket.inputStream    //入力ストリーム
val rcvBuffer: ByteArray = ByteArray(RCV_BUF_SIZE)  //受信バッファ

//受信データ読出し
val numBytes = inStream.read(rcvBuffer, 0, RCV_BUF_SIZE)

【説明】
2行目:
BluetoothSocketの入力ストリームを取得します。(mmSocketはBluetoothSocketです)

6行目:
受信データをinStream.read()メソッドで呼び出します。このメソッドの引数と戻り値は以下の通りです。
◎引数
第1引数(本例ではrcvBuffer): 受信データを読出し格納するバイトアレイ
第2引数(本例では0): オフセット
第3引数(本例ではRCV_BUF_SIZE): 受信データを読み出す最大バイトサイズ
◎戻り値
読み出したバイト数

⑦データ送信

データ送信は、BluetoothSocketの出力ストリームを取得して、そのストリームに対するwrite()メソッドで行います。下記コードでmmSocketはBluetoothSocketです。

val data: String = "CW\n"                //送信データ
val byteData = data.toByteArray()        //バイト配列にする
val outStream = mmSocket?.outputStream   //出力ストリーム作成
outStream?.write(byteData)               //データ送信

5-5.MainActivity.ktの全プログラムコード

以下に作成したKotlinプログラム(MainActivity.kt)の全プログラムコードを示します。

class MainActivity : AppCompatActivity() {

    //Bluetooth接続中フラグ
    private var connectFlag: Boolean = false

    //BluetoothAdapterの取得
    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()

    //Bluetooth接続スレッドのインスタンス
    private lateinit var connectThread: ConnectThread

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)  //画面表示

        //BluetoothAdapterの確認
        if (bluetoothAdapter == null){
            Toast.makeText(this@MainActivity, "Bluetooth機能ありません", Toast.LENGTH_LONG).show()
        }else{
            Toast.makeText(this@MainActivity, "Bluetooth OK", Toast.LENGTH_LONG).show()
        }

        //-----Bluetooth接続のパミッションを要求する----
        //API30より大きいバージョンではパミッション要求
        if (Build.VERSION.SDK_INT > 30) {
            if (ContextCompat.checkSelfPermission(
                    this,
                    android.Manifest.permission.BLUETOOTH_CONNECT
                )
                != PackageManager.PERMISSION_GRANTED
            ) {
                requestPermissionLauncher.launch(android.Manifest.permission.BLUETOOTH_CONNECT)
            }

            //Bluetooth接続パミッション許可待ち処理
            var No: Int = 0
            while (true) {
                if (ContextCompat.checkSelfPermission(
                        this,
                        android.Manifest.permission.BLUETOOTH_CONNECT
                    ) == PackageManager.PERMISSION_GRANTED
                ) {
                    break
                }

                No++
                if (No > 500) {      //5秒経過
                    Toast.makeText(this@MainActivity, "Bluetooth接続が許可されません", Toast.LENGTH_LONG)
                        .show()
                    break;
                }
                Thread.sleep(10)   //10msec待ち
            }
        }

        //Bluetooth有効確認
        if (bluetoothAdapter?.isEnabled == true){
            Toast.makeText(this@MainActivity, "Bluetooth有効", Toast.LENGTH_LONG).show()
        }else{
            Toast.makeText(this@MainActivity, "Bluetooth無効となっています", Toast.LENGTH_LONG).show()
            //Bluetooth無効となっている場合は、Bluetooth有効化要求をする
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            btActivityResultLuncher.launch(enableBtIntent)
        }

        //接続ボタンのonClickListenerを設定
        val con_button = findViewById<Button>(R.id.connectButton)
        con_button.setOnClickListener(con_buttonOnClickListener())

        //切断ボタンのonClickListenerを設定
        val discon_button = findViewById<Button>(R.id.disconnectButton)
        discon_button.setOnClickListener(discon_buttonOnClickListener())

        //CWボタンのonTouchListenerを設定
        val cw_button = findViewById<Button>(R.id.cwButton)
        cw_button.setOnTouchListener(cw_buttonOnTouchListener())

        //CCWボタンのonTouchListenerを設定
        val ccw_button = findViewById<Button>(R.id.ccwButton)
        ccw_button.setOnTouchListener(ccw_buttonOnTouchListener())
    }

    //Bluetoothパミッション要求に対するコールバック
    private val requestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
            if (isGranted) {
                Toast.makeText(this@MainActivity, "パミッションが承諾されました", Toast.LENGTH_LONG).show()
            } else {
                Toast.makeText(this@MainActivity, "パミッションが拒否されました", Toast.LENGTH_LONG).show()
            }
        }

    //Blutooth有効化要求に対するコールバック
    private val btActivityResultLuncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode == RESULT_OK){
                Toast.makeText(this@MainActivity, "Bluetoothが有効化されました", Toast.LENGTH_LONG).show()
            }
        }

    //接続ボタン押された時のonClickListener
    private inner class con_buttonOnClickListener : View.OnClickListener {
        override fun onClick(v: View?) {
             if (connectFlag == false) {      //接続中で無い?
                val btAddEditText = findViewById<EditText>(R.id.btAddEditText)
                //接続先のBluetoothアドレス指定
                val device: BluetoothDevice? = bluetoothAdapter?.getRemoteDevice(btAddEditText.text.toString())

                //接続スレッドスタート
                connectThread = ConnectThread(device!!)
                connectThread.start()
            }
        }
    }

    //切断ボタン押された時のonClickListener
    private inner class discon_buttonOnClickListener : View.OnClickListener {
        override fun onClick(v: View?) {
            try {
                connectThread.cancel()
            } catch (e: IOException) {
                return
            }
            connectFlag = false     //接続フラグクリア
        }
    }

    //CWボタンのonTouchListener
    private inner class cw_buttonOnTouchListener : View.OnTouchListener{
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            if (connectFlag == true) {   //接続中?
                if (event?.action == MotionEvent.ACTION_DOWN) {         //ボタン押された
                    val data: String = "CW\n"
                    val byteData = data.toByteArray()   //バイト配列にする
                    connectThread.write(byteData)       //"CW\n"送信
                } else if (event?.action == MotionEvent.ACTION_UP) {    //ボタン離された
                    val data: String = "STOP\n"
                    val byteData = data.toByteArray()   //バイト配列にする
                    connectThread.write(byteData)       //"STOP\n"送信
                }
            }
            return true
        }
    }

    //CCWボタンのonTouchLitener
    private inner class ccw_buttonOnTouchListener : View.OnTouchListener{
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            if (connectFlag == true) {   //接続中?
                if (event?.action == MotionEvent.ACTION_DOWN) {         //ボタン押された
                    val data: String = "CCW\n"
                    val byteData = data.toByteArray()   //バイト配列にする
                    connectThread.write(byteData)       //"CCW\n"送信
                } else if (event?.action == MotionEvent.ACTION_UP) {    //ボタン離された
                    val data: String = "STOP\n"
                    val byteData = data.toByteArray()   //バイト配列にする
                    connectThread.write(byteData)       //"STOP\n"送信
                }
            }
            return true
        }
    }

    //--------Bluetooth接続スレッドクラス------------------
    private inner class ConnectThread(val device: BluetoothDevice) : Thread() {

        //BluetoothSocket
        private lateinit var mmSocket: BluetoothSocket

        public override fun run() {
            //シリアルポートプロファイルのUUID
            val MY_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");
            //Bluetoothソケットの取得
            mmSocket = this.device.createRfcommSocketToServiceRecord(MY_UUID)

            try {
                //Bluetoothソケット接続
                mmSocket.connect()
            } catch (e: IOException) {
                runOnUiThread {
                    Toast.makeText(this@MainActivity, "Bluetooth接続できません", Toast.LENGTH_LONG).show()
                }
                return
            }

            //接続成功表示
            runOnUiThread {
                 Toast.makeText(this@MainActivity, "Bluetooth接続しました", Toast.LENGTH_LONG).show()
            }

            //受信処理の実施
            rcvPosData() //現在位置データ受信処理
        }

        //Bluetooth切断処理
        fun cancel(){
            try {
                mmSocket?.close()       //切断処理実施
            }catch (e:IOException){
                return
            }

            runOnUiThread {
                Toast.makeText(this@MainActivity, "Bluetooth切断されました", Toast.LENGTH_LONG).show()
            }
        }

        //データ送信処理
        fun write(bytes: ByteArray){
            try {
                val outStream = mmSocket?.outputStream
                outStream?.write(bytes) //データ送信
            }catch (e: IOException){
                runOnUiThread {
                    Toast.makeText(this@MainActivity, "データ送信出来ません", Toast.LENGTH_LONG).show()
                }
            }
        }

        //現在位置データ受信して表示する処理
        fun rcvPosData(){
            val RCV_BUF_SIZE: Int = 1024    //受信バッファサイズ
            val inStream: InputStream = mmSocket.inputStream    //入力ストリーム
            val rcvBuffer: ByteArray = ByteArray(RCV_BUF_SIZE)  //受信バッファ
            var numBytes: Int = 0                               //受信バイト数
            var rcvString: String? = null                       //受信データ文字列

            runOnUiThread {
                Toast.makeText(this@MainActivity, "受信スタートしました", Toast.LENGTH_LONG).show()
            }

            connectFlag = true  //接続中フラグセット

            while (true){
                try{
                    //受信データ読出し
                    numBytes = inStream.read(rcvBuffer, 0, RCV_BUF_SIZE)
                }catch (e: IOException){
                    break;
                }

                //受信データを文字列に変換する
                var rcvData: String = String(rcvBuffer, 0, numBytes)

                //前回までに受信したデータと結合
                if (rcvString == null){
                    rcvString = rcvData
                }else{
                    rcvString = rcvString + rcvData
                }

                //ターミネータ"\r\n"検索
                val termPos = rcvString.lastIndexOf("\r\n")

                if (termPos != -1) {  //"\r\n"存在?
                    //"X:"を検索する
                    val x_pos = rcvString.lastIndexOf("X:")

                    //"X:"が存在して"\r\n"より前に存在
                    if ((x_pos != -1) && (x_pos < termPos)){
                        //数値の部分を抜き出す
                        val posData: String = rcvString.substring(x_pos + 2, termPos)
                        runOnUiThread{
                            val posTextView = findViewById<TextView>(R.id.posTextView)
                            posTextView.text = "X " + posData
                        }
                    }

                    //受信データストリングクリア
                    rcvString = null;
                }
            }

            cancel()        //Bluetooth切断
            connectFlag = false  //接続中フラグクリア
        }
    }
}

6.Raspberry Pi Pico W側のプログラム

以下にRaspberry Pi Pico Wのプログラム全体を示します。プログラム作成ツールはArduino IDEを使用しています。
本プログラムの詳細は「ステッピングモータをBluetoothシリアル通信で制御する実験」の記事で説明していますので、ここではプログラムコードのみ記載します。(ステッピングモータをBluetoothシリアル通信で制御する実験の記事はこちら)

//--Raspberry Pi Pico W Bluetooth JOG運転テストソフト-------------
#include <Stepper.h>
#include <SerialBT.h>

#define RPM_SPEED 10  //モータ回転速度(rpm)
#define STEPS 2048    //モータの1回転のステップ数

//軸の現在位置記憶用データ(符号付きステップ位置)
long current_pos = 0;

//経過時間判断用データ
unsigned long last_time = 0;  //前回の時間記憶
unsigned long delay_time = 0; //処理待ち時間(次の処理をするまでの時間)

//シリアルデータ受信処理用データ
String s_rcvData;  //受信文字列

//回転指令フラグ
bool cw_mov_flag = false;   //CW回転指令フラグ
bool ccw_mov_flag = false;  //CCW回転指令フラグ

//ステッピングモータ初期設定
Stepper stepper(STEPS, 0, 2, 1, 3);  //ステッピングモータ設定(1回転2048ステップ)

void setup() {
  SerialBT.begin(9600); //Bluetoothシリアル通信スタート

  pinMode(0, OUTPUT);   //ステッピングモータ(青線)
  pinMode(1, OUTPUT);   //ステッピングモータ(桃線)
  pinMode(2, OUTPUT);   //ステッピングモータ(黄線)
  pinMode(3, OUTPUT);   //ステッピングモータ(橙線)

  //ステッピングモータ回転速度設定
  stepper.setSpeed(500);  //ステッピングモータの回転速度500rpm

  //処理待ち時間の設定
  delay_time = 60 * 1000 * 1000 / STEPS / RPM_SPEED;
}

void loop() {
  //Blutoothデータ受信処理
  bool res = readBluetooth();
  if (res == true) {    //受信完了
    com_interpreter();  //受信コマンド解釈処理
  }

  //前回処理からの経過時間計算
  unsigned long pass_time;
  unsigned long now_time = micros();  //現在時刻読出し(usec単位)
  if (now_time >= last_time) {
    pass_time = now_time - last_time; //経過時間計算
  } else {
    //オーバフローを加味して経過時間計算
    pass_time = now_time - (0xffffffffUL - last_time + 1);
  }

  //モータ回転処理
  if (pass_time >= delay_time) {  //時間到達?
    last_time = now_time;
    if (cw_mov_flag == true) {    //CW回転?
      stepper.step(-1);           //-1ステップ回転
      current_pos--;              //現在位置マイナス

      //現在位置をPCへシリアル通信
      serial_pos();
    } else if (ccw_mov_flag == true) {  //CCW回転?
      stepper.step(1);                  //1ステップ回転
      current_pos++;                    //現在位置プラス

      //現在位置をPCへシリアル通信
      serial_pos();
    }
  }
}

//現在位置をBluetoothシリアル通信で送信
void serial_pos() {
  String text_x = String(current_pos);
  String text = String("X:" + text_x);
  SerialBT.println(text);  //現在位置をPCへ送信(Bluetooth)
}

//Bluetooth受信処理
//戻り値: ture 受信完了、false 受信未完了
bool readBluetooth() {
  bool rdEndFlag = false;         //受信完了フラグ

  int rNo = SerialBT.available(); //受信データ数読込
  if (rNo > 0) {                  //受信データ有り
    //'\n'までの受信データを読み込む
    s_rcvData = SerialBT.readStringUntil('\n');
    rdEndFlag = true;             //受信完了フラグセット
  } else {
    rdEndFlag = false;            //受信完了フラグリセット
  }

  return rdEndFlag;
}

//パソコンからの受信データコマンド解釈
void com_interpreter() {
  if (s_rcvData.startsWith("CW") == true) {           //"CW"?
    ccw_mov_flag = false;                             //ccw回転フラグリセット
    cw_mov_flag = true;                               //CW回転フラグセット
  } else if (s_rcvData.startsWith("CCW") == true) {   //"CCW"?
    cw_mov_flag = false;                              //cw回転フラグリセット
    ccw_mov_flag = true;                              //CCW回転フラグセット
  } else if (s_rcvData.startsWith("STOP") == true) {  //"STOP"?
    cw_mov_flag = false;                              //cw回転フラグリセット
    ccw_mov_flag = false;                             //ccw回転フラグリセット
  }
}

7.最後に

AndroidスマホとRaspberry Pi Pico WとをBlutoothシリアル通信で接続して、ステッピングモータの動作を操作することが出来ました。無線でのモータの操作のため様々な用途に応用出来ると思います。
今後は消費電力を大幅に削減したBluetooth Low Energyのプログラムの作成にも挑戦したいと思います。


PAGE TOP