M5Stackで測定した温度と気圧をBLEで飛ばし、Androidアプリで受け取ってみる

本記事は、仙台のメイカースペース"[FabLab Senda FLAT](http://fablabsendai-flat.com/)"で、Arduino等を使いながらIoT的機器を作っている集まりで発表した内容をまとめたものです。
この集まりはハード寄りの人が多いため、スマホアプリのBLEの開発についてのさわりについて発表しました。

# 概要

M5StackのENV.Sencorから温度と気圧を読み取り、そのデータをBLEのAdvertisingで飛ばします。
BLEで飛ばした温度と気圧のデータをAndroidアプリで受け取り、アプリ上で表示します。

![overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/08053d12-9944-0d54-e691-9ab36a421f98.png)

BLEでは双方向通信のポイントトゥーポイントと単方向のブロードキャストモードがありますが、今回は簡単なブロードキャストモードモードのみを扱います。

# ソースコード

https://github.com/yhirano/ble_mobile_app_sample/

# 送信側(M5Stack)

## M5Stackとは

M5Stackは液晶ディスプレイ、microSDカードスロット、3つのボタン、USB、Groveのコネクタ5cm四方の基板に搭載しプラスチックのケースに詰め込んだモジュールです。
CPUにはEspressifのESP32が搭載されており、Wi-FiおよびBluetooth v4.2とBLEを使用することができます。

いろいろな拡張基板が公式・非公式を問わずに用意されています。

M5Stack上で動くソフトの開発はEspressifのESP-IDF(C/C++言語)のほか、Arduino IDEや、MicroPythonなどを利用できます。

このように、M5StackはIoT機器の試作にはとても重宝するモジュールです。

* [M5GO IoTスターターキット - スイッチサイエンス](https://www.switch-science.com/catalog/3875/)

## M5Stackの準備

本記事では、M5GOに付属の本体とENV.Sensorを使用します。
写真のように本体のPORT AにENV.Sensorを接続します。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/0eb8746c-e974-a411-b84e-2dd28b199489.jpeg">

## M5Stackの開発環境の構築

今回はArduino言語で開発します。
開発環境には[VSCode](https://code.visualstudio.com/)と[PlatformIO IDE](https://platformio.org/)プラグインを使います。

### PlatformIOとは

Arduino言語で開発する際には一般的には[Arduino IDE](https://www.arduino.cc/en/main/software)が使われますが、開発になれてくるとArduino IDEで開発をするのはツラいものがあります。

PlatformIOはいろいろなマイコン+いろいろなフレームワークに対応した高機能エディタにプラグインできる組み込み用IDEです。
VSCodeの拡張としてインストールすることができ、簡単に開発を始めることができます。

PlatformIOはVSCodeの他にもAtom、Vim、CLionといったエディタにも対応しています。

### VSCodeのインストール

[VSCodeのサイト](https://code.visualstudio.com/)からダウンロードし、インストールして下さい。

### VSCodeへのPlatformIO IDE拡張のインストール

VSCodeを起動し、

<img width="1380" alt="p1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/99fb918d-9016-1dcf-717a-2f9f63a1b1c6.png">
<img width="1380" alt="p2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/418ee7c4-e90b-1ca5-6a48-d76c7318de1d.png">

## 使用するライブラリのダウンロード

M5Stack開発用のライブラリと、ENV.Sensorのライブラリをインストールします。

M5Stack開発用のライブラリをインストールします。
VSCodeのPIO Homeのタブより

<img width="1380" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/fa3d348d-8647-bd3e-a38c-27cb632da577.png">
<img width="1380" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/536b4118-f7f0-b608-eb69-f6b70e408c41.png">
<img width="1380" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/f6928770-0d3a-80bc-7d1e-afcf7dcf99c1.png">

次にENV.Sensorのライブラリをインストールします。

<img width="1502" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/1d88b247-08e7-c731-831d-4c20d7d796e7.png">
<img width="1502" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/257b0243-b606-17bc-68ee-e7153715a1bd.png">

BMP280とはENV.Sensorに搭載されている温度・気圧センサーのチップです。
[データシート等はこちら](https://www.bosch-sensortec.com/products/environmental-sensors/pressure-sensors/pressure-sensors-bmp280-1.html)。

## プロジェクトの作成

プロジェクトを作成します。
VSCodeのPIO Homeのタブより

<img width="1380" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/1ebbed64-9c31-1bf9-ef88-7e4e954dbd80.png">
<img width="1380" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/37a92538-1755-a4fa-c228-223feeb1bf5d.png">

各種ファイルが生成されます。

## M5Stackのセンサーの読み取りとBLEでのデータのアドバタイジング

`src/main.cpp`を下記のように書き換えます。
処理の内容についてはコード内のコメントを参照してください。

```c++:src/main.cpp
#include <M5Stack.h>
#include <Adafruit_BMP280.h>

#include <BLEDevice.h>
#include <BLE2902.h>

// BLE上のこのデバイスの名前。適当な名前でOK。
#define BLE_LOCAL_NAME "M5GO Env.Sensor Advertiser"
// BLEのサービスUUID。適当なUUID(ランダムで生成したものがよい)を設定して下さい。
#define BLE_SERVICE_UUID "133fe8d4-5197-4675-9d76-d9bbf2450bb4"
// BLEのCharacteristic UUID。適当なUUID(ランダムで生成したものがよい)を設定して下さい。
#define BLE_CHARACTERISTIC_UUID "0fc10cb8-0518-40dd-b5c3-c4637815de40"

// BMP280のインスタンスを作成
static Adafruit_BMP280 bmp280;

static BLEServer* pBleServer = NULL;
static BLECharacteristic* pBleNotifyCharacteristic = NULL;

// 温度と気圧のデータを指定したバッファーに書き込みます。
// バッファーには、1〜4バイト目にはリトルエンディアンで温度を格納し、
// 5〜8バイト目にはリトルエンディアンで気圧を格納します。
// 
// - buffer uint8_tの8つ分以上のサイズのバッファを指定すること
static void pack(uint8_t* buffer, float temperature, float pressure) {
  int32_t temperature100 = (int32_t)(temperature * 100);
  buffer[0] = temperature100 & 0x000000ff;
  buffer[1] = (temperature100 & 0x0000ff00) >> 8;
  buffer[2] = (temperature100 & 0x00ff0000) >> 16;
  buffer[3] = (temperature100 & 0xff000000) >> 24;

  int32_t pressure100 = (int32_t)(pressure * 100);
  buffer[4] = pressure100 & 0x000000ff;
  buffer[5] = (pressure100 & 0x0000ff00) >> 8;
  buffer[6] = (pressure100 & 0x00ff0000) >> 16;
  buffer[7] = (pressure100 & 0xff000000) >> 24;
}

// 起動時に最初の1回だけ呼ばれる処理は setup 関数内に書きます
void setup() {
  // M5Stackの初期化
  M5.begin();

  // BMP280の初期化。
  // 初期化の結果が bmp280InitResult の変数に格納されます。
  // 引数に指定している BMP280_ADDRESS_ALT はBMP280センサーのI2Cアドレスです。
  bool bmp280InitResult = bmp280.begin(BMP280_ADDRESS_ALT);

  // M5StackのLCD(液晶ディスプレイ)を黒で塗りつぶします
  M5.Lcd.fillScreen(BLACK);
  // M5StackのLCDで表示する文字色を白、背景色を黒に設定します。
  M5.Lcd.setTextColor(WHITE ,BLACK);
  // M5StackのLCDで表示する文字の大きさを設定します。
  M5.Lcd.setTextSize(2);

  // "BMP280" という文字列をLCDの座標 (10, 20) の位置に表示します。
  // LCDの座標系は左上が(0, 0)、右下が(320, 240)です。
  M5.Lcd.setCursor(10, 20);
  M5.Lcd.print("BMP280");
  // "temperature:" という文字列をLCDの座標 (30, 50) の位置に表示します。
  M5.Lcd.setCursor(30, 50);
  M5.Lcd.print("temperature:");
  // "temperature:" という文字列をLCDの座標 (30, 80) の位置に表示します。
  M5.Lcd.setCursor(30, 80);
  M5.Lcd.print("pressure:");

  // BMP280の初期化が失敗した場合(bmp280InitResultがfalseの場合)は…
  if (!bmp280InitResult) {
    // M5StackのLCDで表示する文字色を黄色にします。
    M5.Lcd.setTextColor(YELLOW ,BLACK);
    // "Failed BMP280 init." という文字列をLCDの座標 (10, 200) の位置に表示します。
    M5.Lcd.setCursor(10, 200);
    M5.Lcd.print("Failed BMP280 init.");
  }

  // BLE環境の初期化
  BLEDevice::init(BLE_LOCAL_NAME);
  // BLEサーバの生成
  pBleServer = BLEDevice::createServer();
  // BLEのサービスの生成。引数でサービスUUIDを設定する。
  BLEService* pBleService = pBleServer->createService(BLE_SERVICE_UUID);
  // BLE Characteristicの生成
  pBleNotifyCharacteristic = pBleService->createCharacteristic(
                                // Characteristic UUIDを指定
                                BLE_CHARACTERISTIC_UUID,
                                // このCharacteristicのプロパティを設定
                                BLECharacteristic::PROPERTY_NOTIFY
                             );
  // BLE Characteristicにディスクリプタを設定
  pBleNotifyCharacteristic->addDescriptor(new BLE2902());
  // BLEサービスの開始
  pBleService->start();
  // BLEのアドバタイジングを開始
  pBleServer->getAdvertising()->start();
}

// 電源が入っている間は、この loop 関数で書いた処理が繰り返されます
void loop() {
  // M5StackのボタンA/B/Cの読み取り状態を更新しています。
  // ボタンを使わない場合でも、loop関数の冒頭で M5.update() を呼んでおくといいでしょう。
  M5.update();

  // BMP280から温度を取得します
  float temperature = bmp280.readTemperature();
  // BMP280から気圧を取得します。取得した気圧を100でわることで、hPaの単位になります。
  float pressure = bmp280.readPressure() / 100;

  // M5StackのLCDで表示する文字色を白、背景色を黒に設定します。
  M5.Lcd.setTextColor(WHITE ,BLACK);

  // LCDの座標 (180, 50) の位置に温度を小数二桁で表示します。
  M5.Lcd.setCursor(180, 50);
  M5.Lcd.printf("%.2fC", temperature);
  
  // LCDの座標 (180, 50) の位置に気圧を小数二桁で表示します。
  M5.Lcd.setCursor(180, 80);
  M5.Lcd.printf("%.2fhPa", pressure);

  // BLEでのデータ通知用バッファを定義
  uint8_t dataBuffer[8];
  // 温度と気圧のデータをdataBufferに格納します
  pack(dataBuffer, temperature, pressure);
  // データをBLEに設定し、送信します
  pBleNotifyCharacteristic->setValue(dataBuffer, 8);
  pBleNotifyCharacteristic->notify();

  // 33ミリ秒停止します
  delay(33);
}
```

## プログラムの転送と実行

M5GO本体のUSB-CポートとPCを接続します。
そして、VSCodeの下部の「→」のをクリックするとプログラムが本体に転送され、本体でプログラムが実行されます。
<img width="1458" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/d6da09ac-bd15-db57-e903-09e9a0cdda7a.png">

本体の液晶ディスプレイに温度と気圧が表示されれば成功です。
<img width="75%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/614547b1-df2e-2290-1466-f57b4b2e354c.jpeg">

# 受信側(Android)

## Android端末(実機)の用意

Androidアプリの開発はPC上のAndroidエミュレータでもできますが、エミュレータではBLEがサポートされていませんので、BLEが搭載されたAndroid端末が必要です。

Androidアプリを実機で開発するには、実機で `デバイスの開発者向けオプション` を有効にする必要があります。
端末の **[設定] > [デバイス情報] > [ビルド番号]**(端末によってメニュー階層が異なる場合があります) を7回タップすると `デバイスの開発者向けオプション` を有効にすることができます。

* Android 7.1（API レベル 25）以下: [設定] > [デバイス情報] > [ビルド番号]
* Android 8.0.0（API レベル 26）および Android 8.1.0（API レベル 26）: [設定] > [システム] > [デバイス情報] > [ビルド番号]
* Android 9（API レベル 28）以上: [設定] > [デバイス情報] > [ビルド番号]

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/93f0bbf5-dd0f-cabd-36c1-771cbb308018.png">

`デバイスの開発者向けオプション` を有効にすると **[設定] > [システム] > [詳細設定] > [開発者向けオプション]**(端末によってメニュー階層が異なる場合があります) のメニューが出現します。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/573bf643-ca52-31f6-f758-076485390aa6.png">

Androidアプリ開発のためには、 **[設定] > [システム] > [詳細設定] > [開発者向けオプション]**でオプションをオンにし、 さらに`[USBデバッグ]` オプションもオンにする必要があります。

* Android 7.1（API レベル 25）以下: [設定] > [開発者向けオプション] > [USBデバッグ]
* Android 8.0.0（API レベル 26）および Android 8.1.0（API レベル 26）: [設定] > [システム] > [開発者向けオプション] > [USBデバッグ]
* Android 9（API レベル 28）以上: [設定] > [システム] > [詳細設定] > [開発者向けオプション] > [USBデバッグ]

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/be3faccc-5284-ec22-37b7-433b9302d3f0.png">

詳しくは[グーグルのドキュメント](https://developer.android.com/studio/debug/dev-options?hl=ja)を参照して下さい。

## Android Studioのインストール

Androidアプリの開発にはAndroid Studioを使用します。
Android Studioのインストールは[グーグルのドキュメントと動画](https://developer.android.com/studio/install?hl=ja)を参照して下さい。

## プロジェクトの作成

Android Studioを起動し、

<img width="667" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/ff89fb1f-0da6-9def-b789-1418125e2b21.png">
<img width="1012" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/6a7837b0-8954-3de4-cecc-2b4b1b6f4fd7.png">
<img width="1012" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/a05549fb-f3a2-edea-1363-b07a41d1e872.png">

プロジェクトが作成されます。

## Androidアプリでのデータの受け取りと表示

### app/src/main/AndroidManifest.xml

`app/src/main/AndroidManifest.xml` を書き換えます。

<img width="1770" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/cb738966-3e22-6467-6d19-38d54b7eb9a9.png">

```xml:app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- NOTE: packageは書き換えないでください -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.github.yhirano.ble_mobile_app_sample.android">

    <!-- BLEを搭載した端末でのみインストールできるように設定 -->
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

    <!-- BLEを使用するにはBluetoothのパーミッションが必要 -->
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <!--
      Android 6以降はBLEを使用するには位置情報取得のパーミッションが必要。
      Android 9以下は ACCESS_FINE_LOCATION の代わり にACCESS_COARSE_LOCATION でもOK。
     -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

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

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

</manifest>
```

### app/src/main/res/layout/activity_main.xml

`app/src/main/res/layout/activity_main.xml` を書き換えます。
(このファイルは画面のレイアウトを指定するためのファイルです)

<img width="1770" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/c4bd5b93-1857-c5cd-627e-f24a579058ec.png">

```xml:app/src/main/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">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="センサーデータの表示欄"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

### app/src/main/src/java/your-package-path/MainActivity.kt

`app/src/main/src/java/your-package-path/MainActivity.kt` を書き換えます。

<img width="1770" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/2c7a4010-694e-bcfa-b86c-74c3f9a8cc5e.png">

```kotlin:app/src/main/src/java/your-package-path/MainActivity.kt
package com.github.yhirano.ble_mobile_app_sample.android // NOTE: packageは書き換えないでください

import android.Manifest
import android.bluetooth.*
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.*


class MainActivity : AppCompatActivity() {

    /** 温度と湿度を表示するTextView */
    private val sensorTextView by lazy {
        findViewById<TextView>(R.id.sensorTextView)
    }

    /** RSSI(電波強度)を表示するTextView */
    private val rssiTextView by lazy {
        findViewById<TextView>(R.id.rssiTextView)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (allPermissionsGranted()) {
            // 位置情報のパーミッションが取得できている場合は、BLEのスキャンを開始
            bleScanStart()
        } else {
            // 位置情報のパーミッションが取得できていない場合は、位置情報の取得のパーミッションの許可を求める
            ActivityCompat.requestPermissions(
                this,
                REQUIRED_PERMISSIONS,
                REQUEST_CODE_PERMISSIONS
            )
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                // 位置情報のパーミッションが取得できている場合は、BLEのスキャンを開始
                bleScanStart()
            } else {
                sensorTextView.text = "パーミッションが許可されていません"
                rssiTextView.text = null
            }
        }
    }

    /** REQUIRED_PERMISSIONSで指定したパーミッション全てが許可済みかを取得する */
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }

    /** BLEのスキャンを開始 */
    private fun bleScanStart() {
        val manager: BluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        val adapter = manager.adapter
        if (adapter == null) {
            sensorTextView.text = "Bluetoothがサポートされていません"
            rssiTextView.text = null
            return
        }
        if (!adapter.isEnabled) {
            sensorTextView.text = "Bluetoothの電源が入っていません"
            rssiTextView.text = null
            return
        }
        val bluetoothLeScanner = adapter.bluetoothLeScanner
        // "M5GO Env.Sensor Advertiser" というデバイス名のみの通知を受け取るように設定
        val scanFilter = ScanFilter.Builder()
            .setDeviceName("M5GO Env.Sensor Advertiser")
            .build()
        val scanSettings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
            .build()
        Log.d(TAG, "Start BLE scan.")
        bluetoothLeScanner.startScan(listOf(scanFilter), scanSettings, scanCallback)
    }

    /** スキャンでデバイスが見つかった際のコールバック */
    private val scanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            super.onScanResult(callbackType, result)

            rssiTextView.text = "RSSI(受信信号強度) ${result.rssi}"

            // デバイスのGattサーバに接続
            val bluetoothGatt = result.device.connectGatt(this@MainActivity, false, gattCallback)
            val resultConnectGatt = bluetoothGatt.connect()
            if (resultConnectGatt) {
                Log.d(TAG, "Success to connect gatt.")
            } else {
                Log.w(TAG, "Failed to connect gatt.")
            }
        }
    }

    /** デバイスのGattサーバに接続された際のコールバック */
    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
            super.onConnectionStateChange(gatt, status, newState)

            if (gatt == null) {
                Log.w(TAG, "Gatt is empty. Maybe Bluetooth adapter not initialized.")
                return
            }

            if (newState == BluetoothGatt.STATE_CONNECTED) {
                Log.d(TAG, "Discover services.")
                // GATTサーバのサービスを探索する。
                // サービスが見つかったら onServicesDiscovered が呼ばれる。
                gatt.discoverServices()
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
            super.onServicesDiscovered(gatt, status)

            Log.d(TAG, "Services discovered.")

            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (gatt == null) {
                    Log.w(TAG, "Gatt is empty. Maybe Bluetooth adapter not initialized.")
                    return
                }
                val service = gatt.getService(BLE_SERVICE_UUID)
                val characteristic = service?.getCharacteristic(BLE_CHARACTERISTIC_UUID)
                if (characteristic == null) {
                    Log.w(TAG, "Characteristic is empty. Maybe Bluetooth adapter not initialized.")
                    return
                }

                // Characteristic "0fc10cb8-0518-40dd-b5c3-c4637815de40" のNotifyを監視する。
                // 変化があったら onCharacteristicChanged が呼ばれる。
                gatt.setCharacteristicNotification(characteristic, true)
                val descriptor = characteristic.getDescriptor(
                    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
                )
                descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                gatt.writeDescriptor(descriptor)
            }
        }

        override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) {
            super.onCharacteristicChanged(gatt, characteristic)

            Log.v(TAG, "onCharacteristicChanged")

            this@MainActivity.runOnUiThread {
                val data = Data.parse(characteristic?.value) ?: return@runOnUiThread

                val sb = StringBuilder()
                sb.append("Temperature: ${String.format("%.2f", data.temperature)}\n")
                sb.append("Pressure: ${String.format("%.2f", data.pressure)}")
                sensorTextView.text = sb.toString()
            }
        }
    }

    /** 温度と気圧を持つデータクラス */
    private data class Data(val temperature: Float, val pressure: Float) {
        companion object {
            /**
             * BLEから飛んできたデータをDataクラスにパースする
             */
            fun parse(data: ByteArray?): Data? {
                if (data == null || data.size < 8) {
                    return null
                }

                val temperatureBytes = ByteBuffer.wrap(data, 0, 4)
                val pressureBytes = ByteBuffer.wrap(data, 4, 4)

                val temperature = temperatureBytes.order(ByteOrder.LITTLE_ENDIAN).int.toFloat() / 100.toFloat()
                val pressure = pressureBytes.order(ByteOrder.LITTLE_ENDIAN).int.toFloat() / 100.toFloat()
                return Data(temperature, pressure)
            }
        }
    }

    companion object {
        private val TAG = MainActivity::class.java.simpleName

        private const val REQUEST_CODE_PERMISSIONS = 10

        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)

        /** BLEのサービスUUID */
        private val BLE_SERVICE_UUID = UUID.fromString("133fe8d4-5197-4675-9d76-d9bbf2450bb4")

        /** BLEのCharacteristic UUID */
        private val BLE_CHARACTERISTIC_UUID = UUID.fromString("0fc10cb8-0518-40dd-b5c3-c4637815de40")
    }
}
```

## アプリの実行

<img width="1770" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/bd83c4be-123e-5b37-bd5f-cba511f441e5.png">


アプリを実行すると、M5Stackから飛んできた温度・気圧のデータを受け取り、画面に温度と気圧を表示します。

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/40911faa-60df-5d12-193a-df735073e20a.png">


# BLEを使うアプリ開発での気にすべきこと

## BLEを搭載していない端末のサポートをする

BLEを搭載していない端末のサポートが必要です。
本記事のアプリのようにBLEがない端末でインストールできないようにするか、BLEがない端末で動くように設計をする必要があります。
最近ではBLEがない端末は見なくなったので、BLEがない端末にはインストールできないようにするのもよいかもしれません。

## AndroidアプリでBLEを使用する場合はたくさんの端末でテストする

Android端末によってはBluetoothの挙動が異なっていることが結構あるので、可能な限りいろいろなOSバージョンと端末で確認することをおすすめします。（不特定多数の端末で使うアプリの場合は特に）

iOSアプリは、最新のiOSバージョンから1〜2つ前のバージョンをサポートしておけば市場的には充分で、かつ端末の種類もAndroidに比べれば少ないので、BLEを使うアプリを開発するならiOSの方が楽です。

## AndroidのBLEのAPIはひどい

AndroidのBLEのAPIはひどいと評判です。実際書いててツラいものがあります。
BLEを使うAndroidアプリを開発する際は工数等の見積もりには注意しましょう。

[Android BLE ひどい - Google 検索](https://www.google.com/search?q=Android+BLE+%E3%81%B2%E3%81%A9%E3%81%84)

# 参考文献

M5Stack + BLE

* [M5StackでBLEを使う - Qiita](https://qiita.com/nsawa/items/2afd4ae9c10af87d0133)
* [M5StackでBLE環境センサー端末を作る – Ambient](https://ambidata.io/samples/m5stack/m5stack_ble_sensor/)
* [ESP32 BLE経由で温度を通知 – thousandiy](https://thousandiy.wordpress.com/2017/10/18/esp32-ble_notify/)

PlatformIO

* [マイコン開発するならPlatformIOがめちゃくちゃいいぞ - Qiita](https://qiita.com/JotaroS/items/1930f156aab953194c9a)

PlatformIO + M5Stack

* [M5Stackの開発環境を整える - PlatformIO IDE編 - Qiita](https://qiita.com/lutecia16v/items/1c560bdd7eac7ebeaff7)

Android + BLE

* [BLE視点でまとめるAndroid OSの違い - Qiita](https://qiita.com/ozyozyo/items/351babdf2160d733a306)