本記事でわかること
・注意点含めてAndroidでBluetoothセントラルアプリの作り方がわかります。
以下のようなセントラルサンプルアプリを作成しました。シングルビューです。
※ループGIFです。

こちらは「SCAN/CONNECTボタン」でペリフェラルのスキャンと接続をします。
WRITEで書き込み、READで読み出し、ペリフェラルからのINDICATEとNOTIFYでデータを受信します。
上記セントラルアプリをベースに説明していきます。
※お詫び:WordPressの設定でJavaのコードハイライト追加で詰まっておりハイライト出来ておりません。完了次第修正致します。
なお、コード全文は最下部に書いています。
環境
・AndroidStudio:Chipmunk|2012.2.1 Patch1
・検証端末Androidバージョン:12(APIレベル32)
セントラルの全体の処理の流れ

●初期設定(パーミッションなどは後ほど詳しく説明します)
1 :BluetoothManagerからBluetoothAdapterインスタンス取得
●スキャン
2 :BluetoothAdapterからBluetoothLeScannerインスタンス取得
3 :BluetooothLeScannerでstartScan実行(ScanCallback設定)
●スキャン結果取得
4 :スキャン結果はstartScanに設定したScanCallbackで取得
5 :ScanCallback内で接続対象デバイス発見
6 発見した接続対象デバイスのデバイスアドレス取得
●接続
7 :デバイスアドレスからBluetoothDeviceインスタンス取得(接続先をインスタンスとして取得)
8 :BluetoothDeviceと接続試行
この時接続先デバイスのBluetoothGattをインスタンスとして取得9 :接続試行時にGattCallbackを指定
10:接続結果がGattCallbackで返ってくる
●接続したデバイスに対してBLEを使うために必要な設定を行う11:接続完了後、BluetoothGattのサービス検索開始
12:サービス検索結果はonServicesDiscoveredでコールバックされる
13:見つけたサービスから所望のCharacteristicを取得してインスタンス取得
14:Notify、Indicateインスタンス取得後Descriptor(今回はCCCD)へ書き込みを行う
前提
今回はシングルビューアプリであるためAndroidManifest.xml以外は全てMainActivity.javaへ記述しています。
初期設定
事前準備1:AnddroidManifest.xmlにパーミッション追加
パーミッションに以下を追加
省略
<!--パーミッション-->
<!--BLE:API30以前-->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30"/>
<!--BLE:API30以降-->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
省略
AndroidではAPI30までBluetoothを使うためには位置情報の許可が必要でした。
しかし、API30からBLUETOOTHのパーミッションが追加され位置情報が不要になりました。
今回はセントラルであるため「BLUETOOTH_SCAN」と「BLUETOOTH_CINNECT」を追加しています。
事前準備2:onCreateでパーミッション要求
onCreate内で以下「InitializeBleSetting」を呼びます。
/**
* BLE関連の初期設定
*/
private void InitializeBleSetting() {
//PermissionのArrayList
ArrayList<String> requestPermissions = new ArrayList<>();
//端末がBLEをサポートしているかの確認
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, "お使いの端末はBLEが対応していません", Toast.LENGTH_SHORT).show();
finish();
}
//bluetoothManagerとbluetoothAdapterのインスタンスを作成して取得
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
//パーミッション要求:BLUETOOTH_CONNECT
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
requestPermissions.add(Manifest.permission.BLUETOOTH_CONNECT);
}
//パーミッション要求:BLUETOOTH_SCAN
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
requestPermissions.add(Manifest.permission.BLUETOOTH_SCAN);
}
//パーミッション要求:ACCESS_FINE_LOCATION API30以上では不要(Manifestで30未満で指定しているので30以上であれば呼ばれない)
if (PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PermissionChecker.PERMISSION_GRANTED) {
requestPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
//必要なPermission要求を全て出す
if (!requestPermissions.isEmpty()) {
ActivityCompat.requestPermissions(this, requestPermissions.toArray(new String[0]), REQUEST_MULTI_PERMISSIONS);
}
logTextView.append("BLE初期化完了\n");
logTextView.append("mBluetoothAdapter取得\n");
}
パーミッションの要求は各パーミッション毎に以下で許諾を確認
//パーミッション要求:BLUETOOTH_SCAN
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
requestPermissions.add(Manifest.permission.BLUETOOTH_SCAN);
}
一度に全てのパーミッションを出すことは出来ないのでパーミッションのArrayListを作成して積んでいます。
//PermissionのArrayList
ArrayList<String> requestPermissions = new ArrayList<>();
最後に積んだパーミッションを全て確認
//必要なPermission要求を全て出す
if (!requestPermissions.isEmpty()) {
ActivityCompat.requestPermissions(this, requestPermissions.toArray(new String[0]), REQUEST_MULTI_PERMISSIONS);
}
以下のような画面が表示されます。

これでパーミッションの要求は完了です。
1 :BluetoothManagerからBluetoothAdapterインスタンスを取得
「InitializeBleSetting()」内でBluetoothAdapterのインスタンスを取得します。
private void InitializeBleSetting() {
省略
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
省略
}
スキャン
今回作成したサンプルアプリでは以下「SCAN/CONNECT」ボタン押下で処理が走ります。
その処理の内容を説明します。

スキャン事前処理1:パーミッション確認
スキャンを開始する前にパーミッションの確認を実施します。
public void onClick(View v) {
if (v != null) {
switch (v.getId()) {
case R.id.scanAndConnectButton:
//パーミッションの確認(BLUETOOTH_CONNECT)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
requestBluetoothConnectPermission();
} else {
//パーミッションの確認(BLUETOOTH_SCAN)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
requestBluetoothScanPermission();
} else {
if (Build.VERSION.SDK_INT > 30) {
ScanBleDevice();
} else {
//パーミッションの確認(ACCESS_FINE_LOCATION):APIレベル30未満で必要
if (PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PermissionChecker.PERMISSION_GRANTED) {
requestLocatePermission();
} else {
ScanBleDevice();
}
}
}
}
break;
以下省略
スキャン事前処理:BLE設定の確認
上記コードの「ScanBleDevice()」では以下の処理を行っています。
/**
* BleDeviceをスキャン:以降の処理のトリガーになる。
*/
private void ScanBleDevice() {
//BLE設定がONになっていることの各院n
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
} else {
progressBar.setVisibility(View.VISIBLE);
//アドバタイズしているサービスUUIDでフィルターをかける場合は以下を追加
// isScanOk = false;
// ScanFilter scanFilter = new ScanFilter.Builder()
// .setServiceUuid(ParcelUuid.fromString("アドバタイズしているサービスのUUID"))
// .build();
logTextView.append("スキャン開始\n");
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
}
}
ScanBleDevice処理内で以下の処理をしている理由はBLE設定がONになっていることを確認するためです。
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
もしBLE設定がOFFになっている場合は以下のようなダイアログを出してBLE設定をONにしてもらいます。

BLE設定がOFFのままですとスキャンなど実行できず処理が進まないため、ボタン押下でBLE許可の確認をしています。
注意点:スキャンフィルターに関して
周囲のBLEデバイスをスキャンするとBLEデバイスを発見する毎にコールバックが呼ばれます。
これは消費電力などの観点からあまり良い処理とは言えません。
そのため、BLEデバイスがアドバタイズに入れているServiceのUUIDでフィルターをかけることが可能です。
以下コードのコメントアウトしている部分を有効にすることでコールバックが呼ばれる対象のデバイスを指定することが可能です。
// ScanFilter scanFilter = new ScanFilter.Builder()
// .setServiceUuid(ParcelUuid.fromString("アドバタイズしているサービスのUUID"))
// .build();
logTextView.append("スキャン開始\n");
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
2 :BluetoothAdapterからBluetoothLeScannerインスタンスを取得
ScanBleDevice()内でBluetoothLeScannerインスタンスを取得します。
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
3 :BluetooothLeScannerでstartScan実行(ScanCallback設定)
BluetoothLeScannerインスタンス取得と共にstartSccanでBLEデバイスのスキャンを実行します。
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
スキャン結果取得
4 :スキャン結果はstartScanに設定したScanCallbackで取得
スキャン結果は、BluetoothLeScannerでStartScanを実行する時に指定したScanCallbackでコールバックが返ってきます。
/**
* ScanCallback
* スキャン結果のonScanResultと失敗した時のonScanFailed設定
*/
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
//接続するPeripheralNameが見つかったら接続
if (PERIPHERAL_NAME.equalsIgnoreCase(result.getDevice().getName())) {
//追加したところ
logTextView.append("接続対象デバイス発見\n");
logTextView.append("DeviceName" + result.getDevice().getName() +"\n");
logTextView.append("DeviceAddr" + result.getDevice().getAddress() +"\n");
logTextView.append("RSSI" + result.getRssi() +"\n");
logTextView.append("UUID" + result.getScanRecord().getServiceUuids() +"\n");
logTextView.append("manufactureSpecific" + result.getScanRecord().getManufacturerSpecificData().toString() +"\n");
logTextView.append("AdvertiseServiceUUID" + result.getScanRecord().getServiceUuids() +"\n");
List<ParcelUuid> advertiseService = result.getScanRecord().getServiceUuids();
if (advertiseService != null) {
logTextView.append("AdvertiseServiceUUID_perse" + advertiseService.get(0) +"\n");
}
//スキャン停止
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback); //探索を停止
logTextView.append("スキャン停止\n");
mDeviceAddress = result.getDevice().getAddress();
//接続を行うためにdeviceアドレスを渡す
connect(mDeviceAddress);
Log.e(TAG, "isScanOk:OK");
}
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
}
};
周囲のBLEデバイスが見つかる毎に以下コールバックが呼ばれます。
public void onScanResult(int callbackType, ScanResult result) {}
5 :ScanCallback内でデバイス発見
onScanResult()の引数のresultからBLEデバイスの取得が可能です。
result.getDevice()
デバイスからは以下のような情報が取得可能です。
・デバイス名称:getName()
・デバイスアドレス:getAddress()
など
またresult自体からは以下情報(アドバタイズデータ)が取得可能です。
・RSSI(電波強度)
・UUID
・ManufactureSpecific
・AdvertiseServiceUUID
今回はデバイス名称を指定して接続するデバイスを特定しているため以下箇所で接続デバイスを特定しています。
//ペリフェラルデバイス名称
private static String PERIPHERAL_NAME = "TEST BLE";
省略
if (PERIPHERAL_NAME.equalsIgnoreCase(result.getDevice().getName())) {
6 発見したデバイスのデバイスアドレス取得
デバイスと接続するためにはデバイスアドレスが必要であるため以下でデバイスアドレスを取得します。
mDeviceAddress = result.getDevice().getAddress();
connect()にデバイスアドレスを渡して接続処理を開始します。
connect(mDeviceAddress);
接続
以下connect()で接続処理を実施します。
/**
* Bluetooth LE機器にホストされているGATTサーバーに接続
* @param address デバイスアドレス.
* @return 接続が確立出来たらtrue、失敗したらfalse
*/
public boolean connect(final String address) {
if (mBluetoothAdapter == null || address == null) {
logTextView.append("BluetoothAdapterが初期化されていません\n");
return false;
}
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
if (device == null) {
logTextView.append("デバイスが見つからないため接続出来ませんでした\n");
return false;
}
// 前回接続したデバイスに再接続
if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
&& mBluetoothGatt != null) {
logTextView.append("前回接続したデバイスに再接続\n");
if (mBluetoothGatt.connect()) {
logTextView.append("接続中\n");
mConnectionState = STATE_CONNECTING;
return true;
} else {
return false;
}
}
// デバイスに直接接続
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
logTextView.append("接続中\n");
mBluetoothDeviceAddress = address;
mConnectionState = STATE_CONNECTING;
return true;
}
7 :デバイスアドレスからBluetoothDeviceインスタンス取得(接続先をインスタンスとして取得
connect内の以下箇所でBluetoothDeviceインスタンスを取得します。
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
8 :BluetoothDeviceと接続試行この時接続先デバイスのBluetoothGattをインスタンスとして取得
以下箇所で接続先デバイスのGATTサーバーと接続を行い、BluetoothGATTをインスタンスとして取得します。
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
9 :接続試行時にGattCallbackを指定
接続試行時にGattCallBackを指定していますが、以降の処理(接続結果の取得、Service取得、Characteristicの検索・取得)は全てこのGattCallBackで行います。
再掲
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
10:接続結果がGattCallbackで返ってくる
接続が完了(接続状態が変化)した場合、以下コードの「onConnectionStateChange」が呼ばれ、引数の「int new State」に変化した接続状態が入っています。
内部変数(mConnectionState)で持っている接続状態を接続中にし、UIの更新などを行っています。
/**
* CallBack
* GATTの処理関係
* Peripheralへの接続,切断,データのやりとり
*/
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
//接続状態が変化した時に呼ばれるコールバック
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
String intentAction;
//接続状態に変化した場合
if (newState == BluetoothProfile.STATE_CONNECTED) {
//Service内部の接続状態を変更
logTextView.append("接続完了\n");
mConnectionState = STATE_CONNECTED;
progressBar.setVisibility(View.INVISIBLE);
logTextView.append("サービス探索開始\n");
mBluetoothGatt.discoverServices();
//ボタンの表示を更新(接続完了)
runOnUiThread(new Runnable() {
@Override
public void run() {
scanAndConnectButton.setEnabled(false);
disconnectButton.setEnabled(true);
connectTextView.setText("接続状態:接続中");
}
});
//切断状態に変化した時
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
//Service内部の接続状態を変更
mConnectionState = STATE_DISCONNECTED;
logTextView.append("切断\n");
//ボタンの表示を更新(切断)
runOnUiThread(new Runnable() {
@Override
public void run() {
scanAndConnectButton.setEnabled(true);
disconnectButton.setEnabled(false);
writeButton.setEnabled(false);
readButton.setEnabled(false);
isNotifyDescripterOk = false;
connectTextView.setText("接続状態:未接続");
}
});
}
}
接続したデバイスに対してBLEを使うために必要な設定を行う
11:接続完了後、BluetoothGattのサービス検索開始
接続が完了した時に呼ばれる
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
にて、BluetoothGattを通してデバイスが持っているServiceの検索を行います。
mBluetoothGatt.discoverServices();
12:サービス結果はonServicesDiscoveredでコールバックされる
サービスが見つかると以下がコールバックされます。
//スキャンでサービスを発見した時のコールバック
@Overridepublic void onServicesDiscovered(BluetoothGatt gatt, int status) {
13:見つけたサービスから所望のCharacteristicを取得してインスタンス取得
今回接続するデバイスのServiceUUID、その中のCharacteristicのUUID、Write、Read、Notify、Indicateを以下のように設定しています。
//アドバタイズメントサービス
private static String SERVICE_UUID = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE";
//CharacteristicのUUID
//モジュールDATA送信:Write
private static String CHARACTERISTIC_WRITE = "AAAAAAAA-AAAA-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールDATA受信:read
private static final String CHARACTERISTIC_READ = "AAAAAAAA-CCCC-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールDATA受信:Indicate
private static final String CHARACTERISTIC_INDICATE = "AAAAAAAA-EEEE-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールDATA受信:notify
private static final String CHARACTERISTIC_NOTIFY = "AAAAAAAA-DDDD-BBBB-BBBB-BBBBBBBBBBBB";
onServiceDiscovered()のServiceを指定してserviceのインスタンスを取得します。
BluetoothGattService service = gatt.getService(UUID.fromString(SERVICE_UUID));
ServiceからWriteとReadのCharacteristicのインスタンスを取得します。
mBluetoothGattWriteCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_WRITE));
if (mBluetoothGattWriteCharacteristic != null) {
logTextView.append("write Characteristic取得完了\n");
}
//ReadCharacteristicの設定
mBluetoothGattReadCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_READ));
if (mBluetoothGattReadCharacteristic != null) {
logTextView.append("read Characteristic取得完了\n");
}
WriteとReadのCharacteristicインスタンス取得までの処理全体は以下です。
//スキャンでサービスを発見した時のコールバック
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
logTextView.append("onServicesDiscovered\n");
if (status == BluetoothGatt.GATT_SUCCESS) {
BluetoothGattService service = gatt.getService(UUID.fromString(SERVICE_UUID));
if(service != null) {
//WriteCharacteristicの設定
mBluetoothGattWriteCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_WRITE));
if (mBluetoothGattWriteCharacteristic != null) {
logTextView.append("write Characteristic取得完了\n");
}
//ReadCharacteristicの設定
mBluetoothGattReadCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_READ));
if (mBluetoothGattReadCharacteristic != null) {
logTextView.append("read Characteristic取得完了\n");
}
以下省略
14:Notify、Indicateインスタンス取得後CCCDへ書き込みを行う
NotifyとIndicateのCharacteristicは取得するだけでは使えず以下の処理が必要です。
・接続先デバイスに対しての送信許可(CCCDへの書き込み)
今回作成したサンプルアプリではIndicate→Notifyの順に設定していますが、逆でも問題ありません。
Indicate
まずはCharacteristicのインスタンスを取得します
//IndicateCharacteristicの設定
mBluetoothGattIndicateCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_INDICATE));
CCCDへの書き込みのためDescriptoreを取得し、ENABLE_INDICATION_VALUEを設定
※Descriptor取得時のUUID「00002902-0000-1000-8000-00805f9b34fb」は固定値です。他の値では取得出来ません。
BluetoothGattDescriptor indicateDescriptor = mBluetoothGattIndicateCharacteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
indicateDescriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
BluetoothGattにNotifyをEnableし、gattにDescriptoreの書き込み(CCCDへの書き込み)を行います。
boolean registered = gatt.setCharacteristicNotification(mBluetoothGattIndicateCharacteristic, true);
boolean descriptoreWrite = gatt.writeDescriptor(indicateDescriptor);
if (descriptoreWrite) {
logTextView.append("indicate設定完了\n");
} else {
logTextView.append("indicate設定失敗\n");
}
Notify
Indicateとやることは同じです。
まずはCharacteristicのインスタンスを取得し、BluetoothGattにNotifyをEnableします。
//NotifyCharacteristicの設定
mBluetoothGattNotifyCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_NOTIFY));
// Notification を要求する
if (mBluetoothGattNotifyCharacteristic != null) {
boolean registered = gatt.setCharacteristicNotification(mBluetoothGattNotifyCharacteristic, true);
//Descriptorへの書き込みはonDescriptorWriteで実行
}
gattに対してDescriptoreの書き込み(CCCDへの書き込み)は、先に処理をしているIndicateのDescriptoreの書き込みが完了後に行います。
Descriptoreの書き込みが完了すると以下メソッドがコールされます。
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {}
上記の中でDescriptoreへの書き込みを実行します。
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
BluetoothGattDescriptor notifyDescriptor = mBluetoothGattNotifyCharacteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
notifyDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
if(isNotifyDescripterOk == false){
boolean descriptoreWrite = gatt.writeDescriptor(notifyDescriptor);
if (descriptoreWrite) {
logTextView.append("Notigy設定完了\n");
isNotifyDescripterOk = true;
} else {
logTextView.append("Notigy設定失敗\n");
}
}
}
注意点:なぜDescriptoreへの書き込みを前回の書き込み完了まで待つ必要があるのか?
理由は単純でDescriptoreへの書き込み処理は一度に1つの未処理リクエストしか持つことができません。したがって、書き込み要求を発行する場合は、書き込み要求を発行する前に書き込み完了応答を待つ必要があります。
そのため、連続でDescriptoreへの書き込み要求を行うと必ず失敗します。
私はここで結構ハマりました。。。
これでBLEデバイスと通信するための準備全て完了!Write、Readは?
Write処理
取得したWriteCharacteristicにsetValueで書き込む値をセットし
BluetoothGattのwriteCharacteristicメソッドでWriteが可能です。
本サンプルでは”write from Android”をバイト列に変換して送信しています。
public void write(){
String strData = "write from Android";
byte[] bytes = strData.getBytes(); // 書き込むバイト列
mBluetoothGattWriteCharacteristic.setValue(bytes);
boolean writeSuccess = mBluetoothGatt.writeCharacteristic(mBluetoothGattWriteCharacteristic);
}
書き込みが完了すると以下メソッドが呼ばれます。(BluetoothGattCallback内の処理です。)
//書き込みが完了した時に呼ばれる
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
logTextView.append("write Success\n");
}
Read処理
取得したReadCharacteristicに対して、BluetoothGATTのreadCharacteristicメソッドで読み出しが可能です。
public void read(){
mBluetoothGatt.readCharacteristic(mBluetoothGattReadCharacteristic);
}
まとめ及び注意点
・Android30を境に必要なパーミッションが違うので気をつけること
・Descriptoreへの書き込みは連続で行わないこと
本サンプルアプリと接続可能なペリフェラルアプリ
以下記事で作成したiOSアプリと接続が可能です。
流用方法
以下宣言しているデバイス名称やService、CharacteristicのUUIDを変更、追加、削除などすれば他デバイスでも接続可能だと思います。
//Characteristic
private BluetoothGattCharacteristic mBluetoothGattWriteCharacteristic;
private BluetoothGattCharacteristic mBluetoothGattNotifyCharacteristic;
private BluetoothGattCharacteristic mBluetoothGattIndicateCharacteristic;
private BluetoothGattCharacteristic mBluetoothGattReadCharacteristic;
//ペリフェラルデバイス名称
private static String PERIPHERAL_NAME = "TEST BLE";
//アドバタイズメントサービス
private static String SERVICE_UUID = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE";
//CharacteristicのUUID
//モジュールDATA送信:Write
private static String CHARACTERISTIC_WRITE = "AAAAAAAA-AAAA-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールDATA受信:read
private static final String CHARACTERISTIC_READ = "AAAAAAAA-CCCC-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールDATA受信:Indicate
private static final String CHARACTERISTIC_INDICATE = "AAAAAAAA-EEEE-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールDATA受信:notify
private static final String CHARACTERISTIC_NOTIFY = "AAAAAAAA-DDDD-BBBB-BBBB-BBBBBBBBBBBB";
コード全文
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//Permission
private static final int REQUEST_ENABLE_BT = 1;
private static final int REQUEST_BT_CONNECT_PERMISSION = 3;
private static final int REQUEST_BT_SCAN_PERMISSION = 4;
private static final int REQUEST_MULTI_PERMISSIONS = 5;
//BLE
private BluetoothAdapter mBluetoothAdapter;
private String mBluetoothDeviceAddress;
private BluetoothGatt mBluetoothGatt;
public int mConnectionState = STATE_DISCONNECTED;
private String mDeviceAddress;
private Boolean isNotifyDescripterOk = false;
//Characteristic
private BluetoothGattCharacteristic mBluetoothGattWriteCharacteristic;
private BluetoothGattCharacteristic mBluetoothGattNotifyCharacteristic;
private BluetoothGattCharacteristic mBluetoothGattIndicateCharacteristic;
private BluetoothGattCharacteristic mBluetoothGattReadCharacteristic;
//mConnectionStateのステータス
public static final int STATE_DISCONNECTED = 0;
public static final int STATE_CONNECTING = 1;
public static final int STATE_CONNECTED = 2;
//ペリフェラルデバイス名称
private static String PERIPHERAL_NAME = "TEST BLE";
//アドバタイズメントサービス
private static String SERVICE_UUID = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE";
//CharacteristicのUUID
//モジュールDATA送信:Write
private static String CHARACTERISTIC_WRITE = "AAAAAAAA-AAAA-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールDATA受信:read
private static final String CHARACTERISTIC_READ = "AAAAAAAA-CCCC-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールDATA受信:Indicate
private static final String CHARACTERISTIC_INDICATE = "AAAAAAAA-EEEE-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールDATA受信:notify
private static final String CHARACTERISTIC_NOTIFY = "AAAAAAAA-DDDD-BBBB-BBBB-BBBBBBBBBBBB";
//モジュールの宣言
private TextView connectTextView, logTextView;
private Button scanAndConnectButton, disconnectButton, writeButton, writeWithoutResponseButton, readButton;
private ProgressBar progressBar;
//デバッグ用のTAG
private String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Buttonの宣言とsetOnCkickListenerのセット
scanAndConnectButton = findViewById(R.id.scanAndConnectButton);
scanAndConnectButton.setOnClickListener(this);
disconnectButton = findViewById(R.id.disconnectButton);
disconnectButton.setOnClickListener(this);
writeButton = findViewById(R.id.writeButton);
writeButton.setOnClickListener(this);
readButton = findViewById(R.id.readButton);
readButton.setOnClickListener(this);
//scanAndConnectButton以外初期はdisable
disconnectButton.setEnabled(false);
writeButton.setEnabled(false);
readButton.setEnabled(false);
//TextView
connectTextView = findViewById(R.id.connectTextView);
logTextView = findViewById(R.id.logTextView);
//スクロール可能にする
logTextView.setMovementMethod(new ScrollingMovementMethod());
//ProgressBar
progressBar = findViewById(R.id.progressBar);
progressBar.setVisibility(View.INVISIBLE);
//BLE機能の初期化
InitializeBleSetting();
}
//setOnCkickListernerのセット
@Override
public void onClick(View v) {
if (v != null) {
switch (v.getId()) {
case R.id.scanAndConnectButton:
//パーミッションの確認(BLUETOOTH_CONNECT)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
requestBluetoothConnectPermission();
} else {
//パーミッションの確認(BLUETOOTH_SCAN)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
requestBluetoothScanPermission();
} else {
if (Build.VERSION.SDK_INT > 30) {
ScanBleDevice();
} else {
//パーミッションの確認(ACCESS_FINE_LOCATION):APIレベル30未満で必要
if (PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PermissionChecker.PERMISSION_GRANTED) {
requestLocatePermission();
} else {
ScanBleDevice();
}
}
}
}
break;
case R.id.writeButton:
write();
break;
case R.id.disconnectButton:
disconnect();
break;
case R.id.readButton:
read();
break;
default:
break;
}
}
}
/**
* BLE関連の初期設定
*/
private void InitializeBleSetting() {
//PermissionのArrayList
ArrayList<String> requestPermissions = new ArrayList<>();
//端末がBLEをサポートしているかの確認
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, "お使いの端末はBLEが対応していません", Toast.LENGTH_SHORT).show();
finish();
}
//bluetoothManagerとbluetoothAdapterのインスタンスを作成して取得
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
//パーミッション要求:BLUETOOTH_CONNECT
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
requestPermissions.add(Manifest.permission.BLUETOOTH_CONNECT);
}
//パーミッション要求:BLUETOOTH_SCAN
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
requestPermissions.add(Manifest.permission.BLUETOOTH_SCAN);
}
//パーミッション要求:ACCESS_FINE_LOCATION API30以上では不要(Manifestで30未満で指定しているので30以上であれば呼ばれない)
if (PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PermissionChecker.PERMISSION_GRANTED) {
requestPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
//必要なPermission要求を全て出す
if (!requestPermissions.isEmpty()) {
ActivityCompat.requestPermissions(this, requestPermissions.toArray(new String[0]), REQUEST_MULTI_PERMISSIONS);
}
logTextView.append("BLE初期化完了\n");
logTextView.append("mBluetoothAdapter取得\n");
}
/**
* 位置情報のPermisstionの許可ダイアログを要求する関数
*/
private void requestLocatePermission() {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("パーミッションの追加説明")
.setMessage("このアプリを使うには位置情報の許可が必要です")
.setPositiveButton("設定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
openSettings();
}
})
.setNegativeButton("いいえ", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//何もしない
}
})
.create()
.show();
}
}
/**
* BLUETOOTH_CONNECTのPermisstionの許可を要求する関数
*/
private void requestBluetoothConnectPermission() {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.BLUETOOTH_CONNECT)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("パーミッションの追加説明")
.setMessage("このアプリを使うにはBLEの許可が必要です")
.setPositiveButton("設定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
openSettings();
// ActivityCompat.requestPermissions(MainActivity.this,
// new String[]{Manifest.permission.BLUETOOTH_CONNECT},
// REQUEST_BT_CONNECT_PERMISSION);
}
})
.setNegativeButton("いいえ", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.create()
.show();
}
}
/**
* BLUETOOTH_SCANのPermisstionの許可を要求する関数
*/
private void requestBluetoothScanPermission() {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.BLUETOOTH_SCAN)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("パーミッションの追加説明")
.setMessage("このアプリを使うにはBLEの許可が必要です")
.setPositiveButton("設定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
openSettings();
// ActivityCompat.requestPermissions(MainActivity.this,
// new String[]{Manifest.permission.BLUETOOTH_SCAN},
// REQUEST_BT_SCAN_PERMISSION);
}
})
.setNegativeButton("いいえ", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.create()
.show();
}
}
/**
* 設定画面を開く
*/
private void openSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
}
/**
* BleDeviceをスキャン:以降の処理のトリガーになる。
*/
private void ScanBleDevice() {
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
} else {
progressBar.setVisibility(View.VISIBLE);
//アドバタイズしているサービスUUIDでフィルターをかける場合は以下を追加
// isScanOk = false;
// ScanFilter scanFilter = new ScanFilter.Builder()
// .setServiceUuid(ParcelUuid.fromString("アドバタイズしているサービスのUUID"))
// .build();
logTextView.append("スキャン開始\n");
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
}
}
/**
* ScanCallback
* スキャン結果のonScanResultと失敗した時のonScanFailed設定
*/
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
//接続するPeripheralNameが見つかったら接続
if (PERIPHERAL_NAME.equalsIgnoreCase(result.getDevice().getName())) {
//追加したところ
logTextView.append("接続対象デバイス発見\n");
logTextView.append("DeviceName" + result.getDevice().getName() +"\n");
logTextView.append("DeviceAddr" + result.getDevice().getAddress() +"\n");
logTextView.append("RSSI" + result.getRssi() +"\n");
logTextView.append("UUID" + result.getScanRecord().getServiceUuids() +"\n");
logTextView.append("manufactureSpecific" + result.getScanRecord().getManufacturerSpecificData().toString() +"\n");
logTextView.append("AdvertiseServiceUUID" + result.getScanRecord().getServiceUuids() +"\n");
List<ParcelUuid> advertiseService = result.getScanRecord().getServiceUuids();
if (advertiseService != null) {
logTextView.append("AdvertiseServiceUUID_perse" + advertiseService.get(0) +"\n");
// Log.i(TAG, "AdvertiseServiceUUID_perse:" + advertiseService.get(0));
// Log.i(TAG, "AdvertiseServiceUUID_perse:" + String.valueOf(advertiseService.get(0)));
}
//スキャン停止
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback); //探索を停止
logTextView.append("スキャン停止\n");
mDeviceAddress = result.getDevice().getAddress();
//接続を行うためにdeviceアドレスを渡す
connect(mDeviceAddress);
Log.e(TAG, "isScanOk:OK");
}
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
}
};
/**
* Bluetooth LE機器にホストされているGATTサーバーに接続
* @param address デバイスアドレス.
* @return 接続が確立出来たらtrue、失敗したらfalse
*/
public boolean connect(final String address) {
if (mBluetoothAdapter == null || address == null) {
logTextView.append("BluetoothAdapterが初期化されていません\n");
return false;
}
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
if (device == null) {
logTextView.append("デバイスが見つからないため接続出来ませんでした\n");
return false;
}
// 前回接続したデバイスに再接続
if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
&& mBluetoothGatt != null) {
logTextView.append("前回接続したデバイスに再接続\n");
if (mBluetoothGatt.connect()) {
logTextView.append("接続中\n");
mConnectionState = STATE_CONNECTING;
return true;
} else {
return false;
}
}
// デバイスに直接接続
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
logTextView.append("接続中\n");
mBluetoothDeviceAddress = address;
mConnectionState = STATE_CONNECTING;
return true;
}
/**
* 既存の接続を切断するか、保留中の接続をキャンセルする
* 切断結果はBluetoothGattCallback#onConnectionStateChange()でコールバックでされる。
*/
public void disconnect() {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
logTextView.append("BluetoothAdapterが初期化されていません\n");
return;
}
logTextView.append("切断試行\n");
mBluetoothGatt.disconnect();
}
/**
* CallBack
* GATTの処理関係
* Peripheralへの接続,切断,データのやりとり
*/
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
//接続状態が変化した時に呼ばれるコールバック
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
String intentAction;
//接続状態に変化した場合
if (newState == BluetoothProfile.STATE_CONNECTED) {
//Service内部の接続状態を変更
logTextView.append("接続完了\n");
mConnectionState = STATE_CONNECTED;
progressBar.setVisibility(View.INVISIBLE);
logTextView.append("サービス探索開始\n");
mBluetoothGatt.discoverServices();
//ボタンの表示を更新(接続完了)
runOnUiThread(new Runnable() {
@Override
public void run() {
scanAndConnectButton.setEnabled(false);
disconnectButton.setEnabled(true);
connectTextView.setText("接続状態:接続中");
}
});
//切断状態に変化した時
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
//Service内部の接続状態を変更
mConnectionState = STATE_DISCONNECTED;
logTextView.append("切断\n");
//ボタンの表示を更新(切断)
runOnUiThread(new Runnable() {
@Override
public void run() {
scanAndConnectButton.setEnabled(true);
disconnectButton.setEnabled(false);
writeButton.setEnabled(false);
readButton.setEnabled(false);
isNotifyDescripterOk = false;
connectTextView.setText("接続状態:未接続");
}
});
}
}
//スキャンでサービスを発見した時のコールバック
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
logTextView.append("onServicesDiscovered\n");
if (status == BluetoothGatt.GATT_SUCCESS) {
BluetoothGattService service = gatt.getService(UUID.fromString(SERVICE_UUID));
if(service != null) {
//WriteCharacteristicの設定
mBluetoothGattWriteCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_WRITE));
if (mBluetoothGattWriteCharacteristic != null) {
logTextView.append("write Characteristic取得完了\n");
}
//ReadCharacteristicの設定
mBluetoothGattReadCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_READ));
if (mBluetoothGattReadCharacteristic != null) {
logTextView.append("read Characteristic取得完了\n");
}
//IndicateCharacteristicの設定
mBluetoothGattIndicateCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_INDICATE));
// Indicate を要求する
if (mBluetoothGattIndicateCharacteristic != null) {
if (mBluetoothGattIndicateCharacteristic != null) {
//mBluetoothGattIndicateCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
BluetoothGattDescriptor indicateDescriptor = mBluetoothGattIndicateCharacteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
indicateDescriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
boolean registered = gatt.setCharacteristicNotification(mBluetoothGattIndicateCharacteristic, true);
boolean descriptoreWrite = gatt.writeDescriptor(indicateDescriptor);
if (descriptoreWrite) {
logTextView.append("indicate設定完了\n");
} else {
logTextView.append("indicate設定失敗\n");
}
}
}
//NotifyCharacteristicの設定
mBluetoothGattNotifyCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_NOTIFY));
// Notification を要求する
if (mBluetoothGattNotifyCharacteristic != null) {
boolean registered = gatt.setCharacteristicNotification(mBluetoothGattNotifyCharacteristic, true);
//Descriptorへの書き込みはonDescriptorWriteで実行
}
}
}
//ボタンの有効化
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mBluetoothGattWriteCharacteristic != null) {
writeButton.setEnabled(true);
}
if (mBluetoothGattReadCharacteristic != null) {
readButton.setEnabled(true);
}
}
});
}
//Descriptorへの書き込みが完了した時のコールバック
//DescriptorWriteへの書き込みが終わると呼ばれる。Descriptorは一つの書き込み中に別の書き込みを行うと必ず失敗する
//今回NotifyとIndicateの2種類の書き込みを行うので,Indicateの書き込みを完了後にIndicateのdiscriptorへの書き込みを行う
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
BluetoothGattDescriptor notifyDescriptor = mBluetoothGattNotifyCharacteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
notifyDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
if(isNotifyDescripterOk == false){
boolean descriptoreWrite = gatt.writeDescriptor(notifyDescriptor);
if (descriptoreWrite) {
logTextView.append("Notigy設定完了\n");
isNotifyDescripterOk = true;
} else {
logTextView.append("Notigy設定失敗\n");
}
}
}
//キャラクタリスティックを読み込んだ時のコールバック
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
byte[] receivedDatabyte = characteristic.getValue();
String strData = new String(receivedDatabyte);
logTextView.append("Read Data Received\n");
logTextView.append("ReadData:"+strData+"\n");
}
}
//キャラクタリスティックの値が変化した時のコールバック(Notify)
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
String characteristicName = characteristic.getUuid().toString() ;
switch (characteristic.getUuid().toString().toUpperCase(Locale.ROOT)){
case CHARACTERISTIC_INDICATE:
logTextView.append("Indicate Data Received\n");
break;
case CHARACTERISTIC_NOTIFY:
logTextView.append("Notigy Data Received\n");
break;
default:
logTextView.append("unkonun Data Received\n");
break;
}
byte[] receivedDatabyte = characteristic.getValue();
String strData = new String(receivedDatabyte);
logTextView.append("ReceivedData:"+strData+"\n");
}
//書き込みが完了した時に呼ばれる
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
logTextView.append("write Success\n");
}
};
public void write(){
String strData = "write from Android";
byte[] bytes = strData.getBytes(); // 書き込むバイト列
mBluetoothGattWriteCharacteristic.setValue(bytes);
boolean writeSuccess = mBluetoothGatt.writeCharacteristic(mBluetoothGattWriteCharacteristic);
}
public void read(){
mBluetoothGatt.readCharacteristic(mBluetoothGattReadCharacteristic);
}
}
Androidの勉強ならUdemyがおすすめです!
私は以下の講座で勉強しています!金額が2400円と安く、それでいて網羅的に理論まで説明してくれるのでとても勉強になります。
The Complete Android 12 Developer Course – Mastering Android英語のコースですが、英語コースの日本語化は以下で説明しているのでご参考にされてください。
最後まで読んで頂きありがとうございました。
もしご参考になりましたら下部にある役に立ったボタン、TwitterなどSNSへの投稿をしていただけると励みになります。
コメント