AndroidスマホとRaspberry Pi Pico WをBLE通信(Bluetooth Low Energy)で接続してステッピングモータを制御する実験を紹介します。スマホ側はAndroid Studioを使用してKotlin言語でアプリを作成しました。Raspberry Pi Pico W側のプログラムはMicroPythonを使用しました。
以前の記事で、スマホ側のアプリとして既存のSerial Bluetooth Terminalを使用してRaspberry Pi Pico WとBLE通信接続してステッピングモータを動作させる実験を紹介しました。
今回Raspberry Pi Pico W側のプログラムは、その記事と同様のものを使用しました。
(Serial Bluetooth Terminalアプリを使用してRaspberry Pi Pico WをBLE通信接続する記事はこちらを参照してください)
1.動作動画
今回作成したスマホアプリを使用してステッピングモータを動作させた動画です。周辺BLEデバイスを検索後に、Raspberry Pi Pico WとBLE通信接続し、スマホ画面の移動位置欄に移動位置ステップ値を入力して「START」ボタンを押すとステッピングモータが指定したステップ位置に回転します。
2.BLE通信に関して
BLEとはBluetooth Low Energyの略です。
Bluetoothには古くから存在するBluetoothクラシックと、後から開発され消費電力を大幅に削減出来るBluetooth Low Energyがあります。それぞれ互換性はありません。
Bluetooth Low Energy (BLE)は親側(セントラル)と子側(ペリフェラル)で通信します。本記事の実験では、スマホがセントラルでRaspberry Pi Pico Wがペリフェラルです。
2-1.BLE通信接続手順
BLE通信ではセントラル側(スマホ側)は、以下の手順でペリフェラル側(Pico W側)と通信接続します。
・周辺BLEデバイスのスキャンを行ってPico Wを捜す
・BLEデバイス(Pico W)のGATTサーバーに接続する
・GATTサーバーから利用可能なServiceを検出する
・利用可能なServiceのCharacteristic(特性)に基づいて接続されたデバイス(Pico W)とデータのやり取りをする
GATTはGeneric Attribute Profileのことで、BLEはこのプロファイルに基づいて通信します。
BLE通信では、GATTクライアントとGATTサーバー間で通信します。GATTクライアントはセントラルのスマホで、GATTサーバーはペリフェラルのPico Wです。
ペリフェラル側(Pico W側)は、その機能をまとめたServiceと言うデータを持っています。Serviceは実際の機能を果たす複数のCharacteristic(特性)と言うデータを内蔵しています。
ServiceとCharacteristicはUUIDと言う16進数のデータが割り付けられています。このUUIDを使って対象とするCharacteristicにデータを書き込んだり、読み出したりすることによって、セントラルとペリフェラル間のデータのやり取りを行うことが出来ます。
今回Pico W側はMicroPythonのBLEペリフェラルサンプルプログラムとして公開されているble_simple_peripheral.pyを使用しました。そのプログラムを使用した場合のServiceは、UART (調歩同期式シリアル通信)で、そのCharacteristicのUUIDとデバイス名称は表1の通りです。
| 項目 | UUID等 |
| UART Service | 6E400001-B5A3-F393-E0A9-E50E24DCCA9E |
| 受信 Characteristic | 6E400002-B5A3-F393-E0A9-E50E24DCCA9E |
| 送信 Characteristic | 6E400003-B5A3-F393-E0A9-E50E24DCCA9E |
| デバイス名称 | mpy-uart |
3.スマホ側BLE通信アプリ作成の概要
スマホ側のアプリはAndroid IDEを使用してKotlin言語で作成しました。今回使用したスマホのAndroidのバージョンは15です。
Android IDEのバージョンは、Narwhal 4 Feature Drop 2025.1.4です。
以下にBLE通信アプリ作成の概要を示します。
3-1.ManifestファイルにPermissonを追加する
BLE機能を使用するアプリはManifestにBluetooth関係のパーミッション等を宣言する必要があります。
app→manifests→AndroidManifest.xmlに以下の様なBluetoothに関するパーミッション等を追加します。(5行目~17行目)
Android 12 未満とAndroid 12 以上で、Bluetoothのアプリ作成上の仕様が若干異なります。パーミッションの記載もAndroidのバージョンによって異なりますので、両方の場合を記載しました。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--BLE用パーミッション等-->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<!--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未満、以上両方に必要な位置情報に関するパーミッション-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--Android12以上に必要な位置情報に関するパーミッション-->
<uses-permission android:name="android.permission.ACCESS_FINE_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"3-2.BLE通信のKotlinプログラムコードの概要
今回作成したアプリのKotlinプログラム(MainActivity.kt)のプログラムコードからBLE通信に関する概要を説明します。
全プログラムは4章を参照して下さい。
①Bluetooth関係のパーミッションのユーザ承認の要求
前記の様にBLE通信を行うためには、manifestsのAndroidManifest.xmlでBluetooth関係のパーミッションの宣言が必要です。
また、Android12 (API31)以降のバージョンでは、Manifestで宣言してあってもアプリ側でBluetooth関係パーミッションのユーザ承認を得ないとアプリが異常停止してしまいますので注意が必要です。
Bluetooth関係のパーミッションの承認をユーザ側に要求する部分のプログラムコードを以下に示します。
//Permissionsの許可要請結果コールバック
private val permLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { }
//Bluetooth関係のPermissionsの許可要請
private fun requestBlePermissions() {
val perms = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= 31) { //API31以上の場合
perms += arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION
)
} else {
perms += arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION
)
}
permLauncher.launch(perms.toTypedArray())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
requestBlePermissions() //Bluetooth関係Permission許可要請
.
.省略
. 【プログラム説明】
2行目:
private val permLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { }
は、20行目のpermLauncher.launch(perms.toTypedArray())で、パーミッションをユーザ側に要求した後の結果のコールバックです。
ここでは、簡単にするために何も処理していません。パーミッションが承諾された等の表示を行う処理を入れると、より良いと思われます。
8行目:
if (Build.VERSION.SDK_INT >= 31) {
は、API31以上(Android 12以上)と未満では必要とするパーミッションが異なるので、その判断をしています。
20行目:
permLauncher.launch(perms.toTypedArray())
は、パーミッションのユーザ側への要求を開始します。
27行目:
requestBlePermissions()
パーミッションをユーザ側に要求する関数です。アプリ起動時に実行します。
②BluetoothAdapterオブジェクトの取得
BLE通信を行うためには最初にBluetoothAdapterのオブジェクトを取得しないとなりません。その部分のプログラムコードを以下に示します。
var bluetoothAdapter: BluetoothAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
.
.省略
.
//BluetoothAdapterの取得
bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
//Bluetoothサポートしているかチェック
if (bluetoothAdapter == null) {
Toast.makeText(this@MainActivity, "Bluetooth機能無", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(this@MainActivity, "Bluetooth機能有", Toast.LENGTH_SHORT).show()
}
}【プログラム説明】
8行目:
bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
で、BluetoothAdapterのオブジェクトを取得します。
10行目:
if (bluetoothAdapter == null) {
は、取得したBlutoothAdapterがnullの場合は、そのスマホはBluetooth機能が無いので、その判断をしています。
③周辺デバイスの検索
スマホの周辺に存在するBLEデバイスを検索 (Scan)します。
何も指定無に検索すると、Pico W以外のデバイスや、Pico WのBluetoothクラシック機能も検出してしまうので、BLEのみのデバイスとPico Wのみを検索します。
Pico WのUARTサービスのUUIDは、”6E400001-B5A3-F393-E0A9-E50E24DCCA9E”なので、そのUUIDでフィルターをかけて検索します。
検索結果で得られたPico WのBluetoothDeviceのオブジェクトを、BLEの通信接続に使用するので記憶しておきます。
//Pico WのBLEのUUID
object BleUartUuid {
val SERVICE: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") //Pico W UARTサービスUUID
val CHAR_RX: UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") //Pico W 受信特性UUID
val CHAR_TX: UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") //Pico W 送信特性UUID
}
private var btDevice: BluetoothDevice? = null
//周辺デバイスの検索
private fun scanLeDevice() {
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
//BLEを検索することを設定
val settings = canSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
//検索するサービスのUUID (Pico WのサービスUUID)を設定
val filters = listOf(ScanFilter.Builder().setServiceUuid(ParcelUuid(BleUartUuid.SERVICE)).build())
//スキャン開始(コールバックleScanCallback)
bluetoothLeScanner?.startScan(filters, settings, leScanCallback)
・
・省略
・
}
//デバイススキャンのコールバックオブジェクト
private val leScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
val device: BluetoothDevice? = result?.device
val uuids = result?.scanRecord?.serviceUuids
val rssi = result?.rssi
val name: String? = result?.device?.name
//BluetoothDeviceのオブジェクト記憶
btDevice = device //接続時に使用のため記憶
・
・検出デバイスの諸元の表示等の処理
・
}
}【プログラム説明】
2行目:
object BleUartUuid {
で、Pico WのUART Serviceと、Characteristic(特性)のUUIDの定義オブジェクトです。
3行目:
val SERVICE: UUID = UUID.fromString(“6E400001-B5A3-F393-E0A9-E50E24DCCA9E”)
が、Pico WのUART ServiceのUUIDです。デバイス検索時に、このUUIDでフィルタ-をかけることによって、Pico Wのみ検索ができます。
12行目:
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
で、bluetoothLeScannerオブジェクトを取得しています。
15行目:
val settings = canSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
は、Bluetoothには古くから存在するBluetoothクラシックと、後から開発され消費電力を大幅に削減出来るBluetooth Low Energy (BLE)があります。
今回の実験では、Bluetooth Low Energy (BLE)を使用します。20行目のbluetoothLeScanner?.startScan(filters, settings, leScanCallback)の引数settingsに上記の値を設定することによって、Bluetooth Low Energy (BLE)に対応したデバイスのみを検索することができます。
17行目:
val filters = listOf(ScanFilter.Builder().setServiceUuid(ParcelUuid(BleUartUuid.SERVICE)).build())
で、Pico WのUART ServiceのUUIDを指定し、20行目のbluetoothLeScanner?.startScan(filters, settings, leScanCallback)実行時に上記filters指定することによって、Pico Wのみ検索することができます。
20行目:
bluetoothLeScanner?.startScan(filters, settings, leScanCallback)
で、周辺デバイスの検索が開始されます。引数leScanCallbackはデバイスが検索された時に呼び出されるコールバックオブジェクトの名称です。
ここでは省略しますが、このstartScan関数を実施すると、stopScan関数を実施するまで延々と周辺デバイスの検索が行われます。適当な時間が経過したらstopScan関数を実効して、デバイスの検索を停止する必要があります。
27行目:
private val leScanCallback: ScanCallback = object : ScanCallback() {
は、20行目のstartScan関数を実施して、デバイスが検出されたときに呼び出されるコールバックオブジェクトです。コールバックオブジェクトの名称はstartScan関数の引き数で指定されています。
31行目~34行目:
検出したデバイスの諸元を呼び出しています。
val device: BluetoothDevice? = result?.device
は、接続時等に使用するBluetoothDeviceのオブジェクトです。
val uuids = result?.scanRecord?.serviceUuids
は、検出されたデバイスのServiceのUUIDです。
val rssi = result?.rssi
は、検出されたデバイスの電波の強さです。
val name: String? = result?.device?.name
は、検出されたデバイスの名称です。Pico Wのデバイスの名称はmpy-uartです。
37行目:
btDevice = device
BLE通信接続するには、接続相手のGATTサーバーと接続します。GATTサーバーとの接続に使用するために、検出された接続対象のデバイスのBluetoothDeviceオブジェクトを記憶しておきます。
④GATTサーバーとの接続とデータの送受信
スマホがRaspberry Pi Pico WとBLE通信接続して、データを送受信するために、Raspberry Pi Pico WのGATTサーバーに接続し、ServiceのCharacteristicを検出し、そのCharacteristicを使用してデータのやり取りをします。
以下に手順を示します。
1.Pico WのGATTサーバーに接続する。
2.GATTサーバーからUART Serviceを検出し、受信に関するCharacteristicと送信に関するCharacteristicを得る。
3.スマホからデータを送信する場合は、受信のCharacteristicのValueにデータを書き込む。
4.送信のCharacteristicのValueが変化した場合に、コールバックするように設定する。
コールバックが発生した時に、送信のCharacteristicからValueを読み込むとPico Wから送られたデータが取得出来る。
具体的な関数使用手順は以下の通りです。
① connectGatt関数を使用して、GATTサーバに接続します。
② GATTサーバへの接続完了コールバックオブジェクトのonConnectionStateChange関数内で、discoverServices関数を使用して、接続相手のServiceを検出します。
③ discoverServices関数実行後にServiceが検出されるとonServicesDiscovered関数が呼び出されるので、getService関数とgetCharacteristic関数を使用して、UART Serviceを得て、その受信と送信に関するCharacteristic(特性)を得ます。
ServiceやCharacteristicを得るのに以下UUIDを使用します。
| 項目 | UUID等 |
| UART Service | 6E400001-B5A3-F393-E0A9-E50E24DCCA9E |
| 受信 Characteristic | 6E400002-B5A3-F393-E0A9-E50E24DCCA9E |
| 送信 Characteristic | 6E400003-B5A3-F393-E0A9-E50E24DCCA9E |
④ スマホ側から見た場合の受信のCharacteristicは、Pico Wの送信のCharacteristicです。このCharacteristicに変化が発生した場合にコールバックするように、setCharacteristicNotification関数を使用して設定します。
このコールバックが呼び出される時は、Pico Wからスマホ側にデータが送られて来た時です。
⑤ データ受信時、コールバックでonCharacteristicChanged関数が呼びだされて、引き数のvalue: ByteArrayが受信データです。(Android 12未満の場合はonCharacteristicChanged関数の引き数にvalueが無くて、characteristic: BluetoothGattCharacteristic引数が存在するので、characteristic.valueで受信データを読み出すことができます。
⑥ データ送信はwriteCharacteristic関数を使用して、Pico Wの受信Characteristicにデータを書き込みます。
以下に具体的なプログラムを示します。
private var gattMem: BluetoothGatt? = null //BluetoothGattの記憶
private var rxChar: BluetoothGattCharacteristic? = null //Pico W受信特製
private var txChar: BluetoothGattCharacteristic? = null //Pico W送信特製
//GATTサーバーに接続
fun connectGATT() {
・
・省略
・
gattMem = null
//GATTサーバーに接続(接続時のコールバックgattCallback)
btDevice?.connectGatt(this@MainActivity, false, gattCallback)
}
//GATTサーバへの接続(connectGatt())に対するコールバックオブジェクト
private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
if (status != BluetoothGatt.GATT_SUCCESS) {
//接続不良発生
・
・接続不良表示等の処理
・
return
}
if (newState == BluetoothProfile.STATE_CONNECTED) { //接続完了
gatt?.discoverServices() //サービスを捜す(コールバックonServicesDiscovered関数)
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
//通信切断発生
・
・通信切断表示等の処理
・
}
}
//gatt?.discoverServices()でサービスが見つかった時に呼び出される関数
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
gattMem = gatt //BluetoothGatt記憶
val service = gatt?.getService(BleUartUuid.SERVICE) //Pico WのUARTサービスゲット
rxChar = service?.getCharacteristic(BleUartUuid.CHAR_RX)//Pico W 受信Charecteristic
txChar = service?.getCharacteristic(BleUartUuid.CHAR_TX)//Pico W 送信Characteristic
//txCharのCharacteristicに変化が有った時(Pico Wからのデータ受信時)に知らせる設定
//データ受信時にonCharacteristicChanged関数が呼び出される
gatt?.setCharacteristicNotification(txChar, true)
connectFlag = true //接続フラグセット
runOnUiThread { //UIスレッド起動
Toast.makeText(this@MainActivity, "接続完了", Toast.LENGTH_SHORT).show()
}
}
}
//txCharのCharacteristicに変化が有った時(Pico Wからのデータ受信時)に呼び出される関数
//Android 12以上の場合(APIレベル>=31)
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
super.onCharacteristicChanged(gatt, characteristic, value)
if (characteristic.uuid == BleUartUuid.CHAR_TX) { //変化CharacteristicのUUIDはCHAR_TX?
//受信データを文字列に変換する(末尾の"\r\n"は除去)
var rcvData = String(value, 0, value.size -2 )
・
・受信データを表示する等の処理
・
}
}
//Android 12未満の場合(APIレベル<=30)
override fun onCharacteristicChanged(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?
) {
super.onCharacteristicChanged(gatt, characteristic)
if (characteristic?.uuid == BleUartUuid.CHAR_TX) { //変化CharacteristicのUUIDはCHAR_TX?
val value = characteristic.value
//受信データを文字列に変換する(末尾の"\r\n"は除去)
var rcvData = String(value!!, 0, value.size - 2)
・
・受信データを表示する等の処理
・
}
}
}
}
//データの送信
fun sendData(data: ByteArray) {
if (gattMem == null) {
return
}
//データ送信
if (Build.VERSION.SDK_INT >= 33) { //Android 13以上の場合
gattMem?.writeCharacteristic(rxChar!!, data, acteristic.WRITE_TYPE_NO_RESPONSE)
} else { //Android 12未満の場合
rxChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
rxChar?.value = data
gattMem?.writeCharacteristic(rxChar)
}
}【プログラム説明】
12行目:
btDevice?.connectGatt(this@MainActivity, false, gattCallback)
は、Pico W側のGATTサーバーに接続する処理です。btDeviceは周辺デバイスの検索で得られたPico WのBluetoothDeviceオブジェクトです。
接続が完了すると、引数で指定したgattCallbackオブジェクトのonConnectionStateChange関数が呼び出されます。
16行目:
private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
は、GATTサーバーへの接続関係が変化した時に呼び出されるコールバックのオブジェクトです。
27行目:
if (newState == BluetoothProfile.STATE_CONNECTED) {
は、BluetoothGattCallbackオブジェクトのonConnectionStateChange関数の引き数newStateがBluetoothProfile.STATE_CONNECTEDの時、GATTサーバーへの接続が完了で、その判断処理です。
28行目:
gatt?.discoverServices()
は、接続相手のサービスの検出をする関数です。サービスが見つかるとonServicesDiscovered関数が呼びだされます。
38行目:
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
は、接続相手のサービスが検出された時に呼び出される関数です。
42行目:
gattMem = gatt
で、onServicesDiscovered関数の引き数のgattはBlutoothGattのオブジェクトで、送信時等に使用するので記憶しておきます。
43行目~45行目:
val service = gatt?.getService(BleUartUuid.SERVICE) //Pico WのUARTサービスゲット
rxChar = service?.getCharacteristic(BleUartUuid.CHAR_RX)//Pico W 受信Charecteristic
txChar = service?.getCharacteristic(BleUartUuid.CHAR_TX)//Pico W 送信Charecteristic
で、Pico W側のServiceと、そのCharacteristicを取得します。
UARTのService、その受信に関するCharacteristic、送信に関するCharacteristicが、それぞれのUUIDを指定することによってが得られます。
49行目:
gatt?.setCharacteristicNotification(txChar, true)
は、送信CharacteristicのtxCharに変化があった時に、コールバックする設定です。
txCharはPico Wから見て送信Characteristicなので、スマホ側から見ると、Pico Wからデータを受信した時のコールバックを指定しています。データを受信するとonCharacteristicChanged関数が呼びだされます。
61行目、70行目、84行目:
override fun onCharacteristicChanged(
は、スマホがPico Wからデータを受信した時に呼び出される関数です。
この関数の引き数はAndroid 12未満と以上では異なるので注意が必要です。
Android 12未満の場合、引数characteristic: BluetoothGattCharacteristicで、characteristic.valueが受信データとなります。Android 12以上の場合は引数value: ByteArrayのvalueが受信データです。
97行目:
fun sendData(data: ByteArray) {
は、データを送信する関数です。
102行目、103行目、107行目:
if (Build.VERSION.SDK_INT >= 33) { //Android 13以上の場合
は、Android 13以上かどうかの判断をしています。
データの送信はwriteCharacteristic関数を使用しますが、Android 13未満とAndroid 13以上では引数が異なります。
Android 13未満では、107行目のgattMem?.writeCharacteristic(rxChar)となり、前もってrxChar?.value = dataで、送信データを指定しておきます。
Android 13以上では、103行目のgattMem?.writeCharacteristic(rxChar!!, data, acteristic.WRITE_TYPE_NO_RESPONSE)で、引数dataで送信データを指定できます。
4.作成したスマホアプリ全体
作成したスマホアプリの全体を以下に示します。
4-1.アプリ画面のレイアウト
アプリ画面のレイアウトは押し釦スイッチ4個とEditTextを1個とTextViewを3個配置しました。以下に画面レイアウトと各Viewのidと役割に関して記載します。

| 名称 | 種類 | id | 役割 |
| STARTボタン | Button | startButton | モータを設定移動位置に回転させる |
| 接続ボタン | Button | connectButton | BLE通信の接続を行う |
| 切断ボタン | Button | disconnectButton | BLE通信を切断する |
| デバイス検索ボタン | Button | searchButton | 周辺BLEデバイスを検索する |
| 現在位置表示 | TextView | posTextView | モータの現在のステップ位置を表示する |
| 移動位置入力 | EditText | movPosEditText | モータが回転するステップ位置を入力する |
| “移動位置” 表示 | TextView | posLabTextView | “移動位置”と言う文字列を表示する |
| デバイス検索結果表示 | TextView | resDspTextView | 周辺BLEデバイスの検索結果を表示する |
| デバイス検索結果表示TextViewのスクロール用ScrollView | ScrollView | scrollView | デバイス検索結果表示のTextViewにスクロール機能を持たせる(TextView部分を<ScrollView…>と</ScrollView>で挟むことによってスクロール可能となる) |
画面レイアウトに対応した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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<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/posLabTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="移動位置"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/posTextView" />
<EditText
android:id="@+id/movPosEditText"
android:layout_width="139dp"
android:layout_height="48dp"
android:layout_marginStart="24dp"
android:layout_marginTop="32dp"
android:ems="10"
android:inputType="text"
android:textSize="24sp"
app:layout_constraintStart_toEndOf="@+id/posLabTextView"
app:layout_constraintTop_toBottomOf="@+id/posTextView" />
<Button
android:id="@+id/startButton"
android:layout_width="147dp"
android:layout_height="74dp"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:text="START"
android:textSize="24sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/movPosEditText" />
<Button
android:id="@+id/connectButton"
android:layout_width="147dp"
android:layout_height="74dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:text="接続"
android:textSize="34sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/startButton" />
<Button
android:id="@+id/disconnectButton"
android:layout_width="147dp"
android:layout_height="74dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:text="切断"
android:textSize="34sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/startButton" />
<Button
android:id="@+id/searchButton"
android:layout_width="147dp"
android:layout_height="74dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:text="デバイス検索"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/connectButton" />
<ScrollView
android:id="@+id/scrollView"
android:layout_width="380dp"
android:layout_height="120dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:fillViewport="true"
android:scrollbars="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/searchButton">
<TextView
android:id="@+id/resDspTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00BCD4"
android:textColor="#000000" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>4-2.Kotlin全プログラム
以下に作成したKotlinプログラム(MainActivity.kt)の全プログラムコードを示します。
class MainActivity : AppCompatActivity() {
private var displayText: String = " " //デバイス検索結果表示用テキスト
private var bluetoothAdapter: BluetoothAdapter? = null
private var connectFlag = false
private var btDevice: BluetoothDevice? = null
private var gattMem: BluetoothGatt? = null //BluetoothGattオブジェクトの記憶
private var rxChar: BluetoothGattCharacteristic? = null
private var txChar: BluetoothGattCharacteristic? = null
//Pico WのBLEのUUID
object BleUartUuid {
val SERVICE: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") //Pico W サービスUUID
val CHAR_RX: UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") //Pico W 受信UUID
val CHAR_TX: UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") //Pico W 送信UUID
}
//Permissionsの許可要請結果コールバック
private val permLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { }
//Bluetooth関係のPermissionsの許可要請
private fun requestBlePermissions() {
val perms = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= 31) { //API31以上の場合
perms += arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION
)
} else {
perms += arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION
)
}
permLauncher.launch(perms.toTypedArray())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
requestBlePermissions() //Bluetooth関係Permission許可要請
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
//Bluetoothサポートしているかチェック
if (bluetoothAdapter == null) {
Toast.makeText(this@MainActivity, "Bluetooth機能無", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(this@MainActivity, "Bluetooth機能有", Toast.LENGTH_SHORT).show()
}
//Bluetooth有効か確認する
if (bluetoothAdapter?.isEnabled == true) {
Toast.makeText(this@MainActivity, "Bluetooth有効", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@MainActivity, "Bluetooth無効", Toast.LENGTH_LONG).show()
}
//-----押し釦スイッチ関係リスナ設定----------
//接続ボタンonClickListenerを設定
val connectButton = findViewById<Button>(R.id.connectButton)
connectButton.setOnClickListener(ConnectButtonOnClickListener())
//切断ボタンonClickListenerを設定
val disconnectButton = findViewById<Button>(R.id.disconnectButton)
disconnectButton.setOnClickListener(DisconnectButtonOnClickListener())
//検索ボタンのonClickListenerを設定
val searchButton = findViewById<Button>(R.id.searchButton)
searchButton.setOnClickListener(SearchButtonOnClickListener())
//STARTボタンのonClickListenerを設定
val startButton = findViewById<Button>(R.id.startButton)
startButton.setOnClickListener(StartButtonOnClickListener())
}
//接続ボタン押された時のonClickListener
private inner class ConnectButtonOnClickListener : View.OnClickListener {
override fun onClick(v: View?) {
if (!connectFlag) { //接続中で無い?
//----BLE接続スレッド-----
Thread {
runOnUiThread {
Toast.makeText(this@MainActivity, "接続開始", Toast.LENGTH_SHORT).show()
}
if (btDevice != null) { //周辺デバイスSCAN完了済み?
connectGATT() //Pico WのGATTサーバーに接続処理
} else {
runOnUiThread {
Toast.makeText(this@MainActivity, "まずデバイス検索して下さい", Toast.LENGTH_SHORT).show()
}
}
}.start()
}
}
}
//切断ボタン押された時のonClickListener
private inner class DisconnectButtonOnClickListener : View.OnClickListener {
override fun onClick(v: View?) {
disconnect() //BLE切断
connectFlag = false //接続フラグクリア
runOnUiThread { //UIスレッド起動
Toast.makeText(this@MainActivity, "BLE切断しました", Toast.LENGTH_LONG).show()
}
}
}
//検索ボタン押された時のonClickListener
private inner class SearchButtonOnClickListener : View.OnClickListener {
override fun onClick(v: View?) {
scanLeDevice() //周辺デバイスのスキャン開始
}
}
//STARTボタンのonClickListener
private inner class StartButtonOnClickListener : View.OnClickListener {
override fun onClick(v: View?) {
if (connectFlag) { //接続中?
val movPosEditText = findViewById<EditText>(R.id.movPosEditText)
val pos: String = movPosEditText.text.toString()
//Int変換して入力データが数値かどうか確認する
try {
val posInt = pos.toInt()
} catch (e: Exception){
Toast.makeText(this@MainActivity, "入力数値異常", Toast.LENGTH_LONG).show()
return
}
val data = "m$pos\r\n"
val byteData = data.toByteArray() //バイト配列にする
sendData(byteData) //"移動位置データ"送信
}
}
}
//周辺デバイススキャン
private fun scanLeDevice() {
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
btDevice = null
//BLEデバイスを検索することを設定
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
//検索するサービスのUUID (Pico WのサービスUUID)を設定
val filters = listOf(ScanFilter.Builder().setServiceUuid(ParcelUuid(BleUartUuid.SERVICE)).build())
//---10秒間周辺デバイス検索-----
Thread { //スレッド
//スキャン開始(コールバックleScanCallback)
bluetoothLeScanner?.startScan(filters, settings, leScanCallback)
//"スキャン開始"とTextViewに表示する
displayText = "スキャン開始" + "\r\n"
val dTextView = findViewById<TextView>(R.id.resDspTextView)
runOnUiThread { //UIスレッド起動
dTextView.text = displayText
}
Thread.sleep(10000) //10m sec待ち
//スキャン停止
bluetoothLeScanner?.stopScan(leScanCallback)
//"スキャン終了"とTextViewに表示
displayText = displayText + "スキャン終了" + "\r\n"
runOnUiThread { //UIスレッド起動
dTextView.text = displayText //TextViewに表示
//一番下にスクロール
val scroll = findViewById<ScrollView>(R.id.scrollView)
scroll.smoothScrollTo(0, dTextView.bottom)
}
}.start()
}
//デバイススキャンのコールバック
private val leScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
val device: BluetoothDevice? = result?.device
val uuids = result?.scanRecord?.serviceUuids
val rssi = result?.rssi
val name: String? = result?.device?.name
btDevice = device //BluetoothDeviceのオブジェクト記憶
//---TextViewにスキャン結果を表示する-------
val dTextView = findViewById<TextView>(R.id.resDspTextView)
uuids?.forEach { uuid ->
val uuidStr = uuid.uuid.toString()
val logText = "Device: ${device?.address}, UUID: $uuidStr, RSSI: $rssi, NAME: $name"
displayText = displayText + logText + "\r\n"
runOnUiThread { //UIスレッド起動
dTextView.text = displayText
//一番下にスクロール
val scroll = findViewById<ScrollView>(R.id.scrollView)
scroll.smoothScrollTo(0, dTextView.bottom)
}
}
}
}
//データの送信
fun sendData(data: ByteArray) {
if (gattMem == null) {
return
}
//データ送信
if (Build.VERSION.SDK_INT >= 33) { //Android 13以上の場合
gattMem?.writeCharacteristic(
rxChar!!,
data,
BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
)
} else { //Android 13未満の場合
rxChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
rxChar?.value = data
gattMem?.writeCharacteristic(rxChar)
}
}
//GATT切断
fun disconnect() {
try {
gattMem?.disconnect()
gattMem?.close()
} catch (e: Exception) {
}
gattMem = null
}
//Pico WのBLE GATTサーバーに接続処理
fun connectGATT() {
disconnect() //前回接続時のBluetoothGattをクローズしておく
gattMem = null
//GATTサーバーに接続(接続時のコールバックgattCallback)
btDevice?.connectGatt(this@MainActivity, false, gattCallback)
}
//GATTサーバへの接続(connectGatt())に対するコールバック
private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
if (status != BluetoothGatt.GATT_SUCCESS) {
connectFlag = false
disconnect()
runOnUiThread { //UIスレッド起動
Toast.makeText(
this@MainActivity, "接続不良発生", Toast.LENGTH_LONG
).show()
}
return
}
if (newState == BluetoothProfile.STATE_CONNECTED) { //接続完了
gatt?.discoverServices() //サービスを捜す(コールバックonServicesDiscovered関数)
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
connectFlag = false
disconnect()
runOnUiThread { //UIスレッド起動
Toast.makeText(this@MainActivity, "GATT切断", Toast.LENGTH_LONG).show()
}
}
}
//gatt?.discoverServices()でサービスが見つかった時に呼び出される関数
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
gattMem = gatt //BluetoothGattオブジェクトの記憶
val service = gatt?.getService(BleUartUuid.SERVICE) //Pico Wのサービスゲット
rxChar = service?.getCharacteristic(BleUartUuid.CHAR_RX)//Pico W 受信キャラ
txChar = service?.getCharacteristic(BleUartUuid.CHAR_TX)//Pico W 送信キャラ
//txCharのCharacteristicに変化が発生した時(Pico Wからのデータ受信時)に知らせる設定
//データ受信時にonCharacteristicChanged関数が呼び出される
gatt?.setCharacteristicNotification(txChar, true)
connectFlag = true //接続フラグセット
runOnUiThread { //UIスレッド起動
Toast.makeText(this@MainActivity, "接続完了", Toast.LENGTH_SHORT).show()
}
}
}
//txCharのCharacteristicに変化が有った時(Pico Wからのデータ受信時)に呼び出される関数
//Android 12以上の場合(APIレベル>=31)
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
super.onCharacteristicChanged(gatt, characteristic, value)
if (characteristic.uuid == BleUartUuid.CHAR_TX) { //変化CharacteristicのUUIDはCHAR_TX?
//受信データ(モータの現在位置)を文字列に変換する(末尾の"\r\n"は除去)
var rcvData = String(value, 0, value.size -2 )
//モータの現在位置をTextViewに表示
val posTextView = findViewById<TextView>(R.id.posTextView)
runOnUiThread { //UIスレッド起動
posTextView.text = buildString {
append("X ")
append(rcvData)
}
}
}
}
//Android 12未満の場合(APIレベル<=30)
override fun onCharacteristicChanged(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?
) {
super.onCharacteristicChanged(gatt, characteristic)
if (characteristic?.uuid == BleUartUuid.CHAR_TX) { //変化CharacteristicのUUIDはCHAR_TX?
val value = characteristic.value
//受信データ(モータの現在位置)を文字列に変換する(末尾の"\r\n"は除去)
var rcvData = String(value!!, 0, value.size - 2)
//モータの現在位置をTextViewに表示
val posTextView = findViewById<TextView>(R.id.posTextView)
runOnUiThread { //UIスレッド起動
posTextView.text = buildString {
append("X ")
append(rcvData)
}
}
}
}
}
}5.Raspberry Pi Pico W側の回路とプログラム
Raspberry Pi Pico W側の回路とプログラムは、以前「Raspberry Pi Pico Wを使用してステッピングモータをBluetooth Low Energy通信で制御する実験」の記事で説明したものと同様です。詳細はその記事を参照して下さい。以下で概要を示します。
(Raspberry Pi Pico Wを使用してステッピングモータをBluetooth Low Energy通信で制御する実験の記事はこちら)
5-1.ステッピングモータに関して
ステッピングモータは28BYJ-48とそのドライバーボードのセットを使用しました。このステッピングモータとドライバーボードは複数のネット通販から購入することが出来ます。



今回使用したステッピングモータの主な仕様は表3の通りです。このモータは1/64のギアで減速されていて、1-2相励磁の場合にステップ角は5.625°/64なので4096ステップでモータ軸が1回転します。最大自起動周波数が500ppsなので、500パルス/sec以下で起動する必要があります。起動後は徐々に加速を行った場合に、最大で1000パルス/secまで仕様上は可能と思われます。
| 項目 | 仕様 |
| 相数 | 4相 |
| 励磁方式 | 1-2相励磁方式 |
| ステップ角 | 5.625°/64 (減速比1/64) |
| 電圧 | 5VDC |
| 相抵抗 | 22Ω ± 7% 25°C |
| 最大応答周波数 | 1000pps |
| 最大自起動周波数 | 500pps |
| 引き込みトルク | 800gf.cm / 5VDC 400pps |
5-2.Pico W 周辺回路
図1にRaspberry Pi Pico W 周辺の回路図を示します。1個のステッピングモータを接続しています。
28BYJ-48ステッピングモータのドライバーボードはテキサスインスツルメンツのULN2003ANと言うダーリントントランジスタアレイが使用されています。
ステッピングモータのドライバーボードの入力信号IN1~IN4をRaspberry Pi Pico WのI/O GP0~GP3に接続しました。
BLE通信を使用してスマホと接続しました。

5-3.Pico WのMicroPythonプログラム
以下にRaspberry Pi Pico Wのプログラム全体を示します。プログラム言語は、MicroPythonを使用しています。
詳細説明はこちらの記事を参照して下さい。
#BLE通信とステッピングモータ駆動テストプログラム
from machine import Pin
import bluetooth
from ble_simple_peripheral import BLESimplePeripheral
import time
#BLEオブジェクト作成
ble = bluetooth.BLE()
#BLESimplePeripheralをBLEオブジェクトとしてインスタンス作成
peri = BLESimplePeripheral(ble)
#ステッピングモータ用出力ピン設定
sp1 = Pin(0, Pin.OUT) #GP0をステッピングモータ出力1とする
sp2 = Pin(1, Pin.OUT) #GP1をステッピングモータ出力2とする
sp3 = Pin(2, Pin.OUT) #GP2をステッピングモータ出力3とする
sp4 = Pin(3, Pin.OUT) #GP3をステッピングモータ出力4とする
#処理待ち時間の設定
STEPS = 4096 #モータ1回転のステップ数
RPM_SPEED = 7 #モータ回転速度(RPM)
step_time = 60 * 1000 / STEPS / RPM_SPEED #モータを1ステップ回転する時間間隔(msec)
mov_flag = False #モータ動作フラグ
mov_pos = 0 #モータ動作目標位置
current_pos = 0 #モータ現在位置
mot_step = 1 #モータステップ位置
#ステッピングモータ1ステップ回転処理
def step_1_rot(step, cw_flag):
ret_step = 0 #リターンステップ番号
#モータの現在のステップ位置による分岐
if step == 1:
if cw_flag == True: #CW移動
sp1.off()
sp2.off()
sp3.on()
sp4.on()
ret_step = 2 #ステップ2へ
else:
sp1.on()
sp2.off()
sp3.off()
sp4.on()
ret_step = 8 #ステップ8へ
elif step == 2:
if cw_flag == True: #CW移動
sp1.off()
sp2.off()
sp3.on()
sp4.off()
ret_step = 3 #ステップ3へ
else:
sp1.off()
sp2.off()
sp3.off()
sp4.on()
ret_step = 1 #ステップ1へ
elif step == 3:
if cw_flag == True: #CW移動
sp1.off()
sp2.on()
sp3.on()
sp4.off()
ret_step = 4 #ステップ4へ
else:
sp1.off()
sp2.off()
sp3.on()
sp4.on()
ret_step = 2 #ステップ2へ
elif step == 4:
if cw_flag == True: #CW移動
sp1.off()
sp2.on()
sp3.off()
sp4.off()
ret_step = 5 #ステップ5へ
else:
sp1.off()
sp2.off()
sp3.on()
sp4.off()
ret_step = 3 #ステップ3へ
elif step == 5:
if cw_flag == True: #CW移動
sp1.on()
sp2.on()
sp3.off()
sp4.off()
ret_step = 6 #ステップ6へ
else:
sp1.off()
sp2.on()
sp3.on()
sp4.off()
ret_step = 4 #ステップ4へ
elif step == 6:
if cw_flag == True: #CW移動
sp1.on()
sp2.off()
sp3.off()
sp4.off()
ret_step = 7 #ステップ7へ
else:
sp1.off()
sp2.on()
sp3.off()
sp4.off()
ret_step = 5 #ステップ5へ
elif step == 7:
if cw_flag == True: #CW移動
sp1.on()
sp2.off()
sp3.off()
sp4.on()
ret_step = 8 #ステップ8へ
else:
sp1.on()
sp2.on()
sp3.off()
sp4.off()
ret_step = 6 #ステップ6へ
elif step == 8:
if cw_flag == True: #CW移動
sp1.off()
sp2.off()
sp3.off()
sp4.on()
ret_step = 1 #ステップ1へ
else:
sp1.on()
sp2.off()
sp3.off()
sp4.off()
ret_step = 7 #ステップ7へ
return ret_step #ステップ位置リターン
#ステッピングモータをposの位置まで回す
def mot_move(pos):
global current_pos
global mot_step
no = 0
while True:
print(current_pos)
if pos > current_pos: #目的位置>現在位置
mot_step = step_1_rot(mot_step, True) #CW方向1ステップ回転
current_pos = current_pos + 1 #現在位置プラス
if pos == current_pos: #位置到達?
break #終了
elif pos < current_pos: #目的位置<現在位置
mot_step = step_1_rot(mot_step, False) #CCW方向1ステップ回転
current_pos = current_pos - 1 #現在位置マイナス
if pos == current_pos: #位置到達
break #終了
else:
break
#10回に1回BLE通信で現在位置を送る
no = no + 1
if no >= 10:
no = 0
send_pos(current_pos) #現在位置送信
time.sleep_ms(int(step_time)) #時期ステップ移動待ち
send_pos(current_pos) #最終現在位置送信
#モータの現在位置をBLE送信
def send_pos(pos):
text = str(pos) + "\r\n"
peri.send(text)
#BLE受信コールバック関数
def rcv_data(data):
global mov_flag
global mov_pos
data_str = data.decode() #受信データをstr型に変換
f_m = data_str.find("m") #"m"の位置検索
f_end = data_str.find("\r\n") #"\r\n"の位置検索
if (f_m != -1 and f_end != -1 and f_m < f_end):
m_data = data_str[f_m + 1 : f_end] #位置データ取り出し
mov_pos = int(m_data) #モータ回転目標位置セット
mov_flag = True #モータ動作フラグセット
#----------メイン処理--------------------
connect_flag = False
#ステッピングモータ初期位置設定
sp1.off()
sp2.off()
sp3.off()
sp4.on()
ret_step = 1
peri.on_write(rcv_data) #BLE受信コールバック関数設定
while True:
if peri.is_connected(): #BLE接続中?
if connect_flag == False:
print("BE接続")
connect_flag = True
if mov_flag == True: #モータ動作要求?
mot_move(mov_pos) #モータ駆動
mov_flag = False
else:
if connect_flag == True:
print("BLE切断")
connect_flag = False6.最後に
AndroidスマホとRaspberry Pi Pico WとをBLE通信で接続して、ステッピングモータの動作を操作することが出来ました。低消費電力無線通信を使用したモータの操作のため様々な用途に応用出来ると思います。