スマホとPicoWをBLE接続する簡単なアプリを作る

環境構築は[前回の記事](https://qiita.com/NKJMeng/items/1c451bb3c07ba4b1e34b)を参照

スマホ＋BLEの基本から確認したいので各要素は次の通り
要素 | 内容
|---|---
画面 | 1画面だけ
ボタン1 | LED ON（Picoにコマンド送信）
ボタン2 | LED OFF（Picoにコマンド送信）
BLEスキャン | ボタンを押すと、周辺のBLEデバイスを探してPicoに接続する
通信方式 | GATT接続、CharacteristicにWrite

スマホアプリの動作（まずはシンプルに）
　├ スマホから周辺のBLEデバイスをスキャンする
　├ PicoTempを見つけてリストに表示する
　└ タップすると接続する

作業の流れ
１．必要なFlutterライブラリ（BLE通信用）を追加する
２．アプリ画面に「LED ON」「LED OFF」ボタンを設置する
３．BLEでPicoに接続してコマンドを送る

## １．BLE通信ライブラリを追加する
使うライブラリは`flutter_blue_plus`

１）左のタブのProjectツリーから'pubspec.yaml'を探してダブルクリックで開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4018803/b87d8147-556d-4f4c-ad1a-203599b28e03.png)
２）`pubspec.yaml`を編集する
「dependencies:」内に `flutter_blue_plus: ^1.15.5`を追加。
　この時、インデントもきちんと再現する。TABは不可。半角スペース2個。
```yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_blue_plus: ^1.15.5 # ←追加した行
```
３）`ctrl+S`でファイルの保存
４）カレントファイルの上部バー付近に出る`Pub get`ボタンを押す
　ログにパッケージ依存関係の変更などが表示される。主な内容は次の通り。
メッセージ | 意味 | 今回どうするか
|---|---|---|
Changed 15 dependencies! | 15個のパッケージの依存関係が変更されました（正常） | ✅ 気にしなくていい
9 packages have newer versions... | 9個のパッケージに新しいバージョンがあるけど今は使わないよ、という警告 | ✅ 今は気にしなくていい
Try flutter pub outdated | もっと知りたかったらコマンドで調べてねって提案してるだけ | （興味あれば後で調べてもOK）
Process finished with exit code 0 | 正常終了（エラーなし） | ✅ 問題なし

問題ないので次へ進める。

## ２．双方とも、BLEスキャンができる設定にする
《AndroidのBLE仕様（Android 6.0以降）について》
・BLE電波からざっくりした位置情報が推測できてしまうことを懸念し、プライバシー保護のためにBLEスキャンをするには位置情報サービスもONにしていないと許可されないというルールがある。

なので、BluetoothだけONにしててもスキャンは動かない。
➡ １）スマホの位置情報サービスを有効化
➡ ２）アプリの位置情報パーミッションを設定

また、単にManifestに書くだけじゃなくアプリ起動中に、ユーザーに「このアプリに位置情報アクセスを許可しますか？」と尋ねて、許可をもらう必要がある
➡ ３）「ランタイムパーミッション要求」の追加

これらが必要なので、位置情報をスキャンできるように次の設定変更を行う

#### １）スマホ設定の、`位置情報サービス`を`ON`

#### ２）アプリ自体に位置情報パーミッションを設定
`AndroidManifest.xml`を編集してパーミッション追加する。
まず、Android Studioの左カラムから`android/app/src/main/AndroidManifest.xml`を開く
```
pico_temp_ble
├─ android
   ├─ app
      ├─ src
         ├─ main
            └─ AndroidManifest.xml　←これ！！
```
`<application>` タグより前（つまり `<manifest>` の直後あたり）に、次の行を追記
```dart
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4018803/4c8319d8-7e75-4839-816c-bed8476b1bad.png)

#### ３）「ランタイムパーミッション要求」の追加

新しくライブラリ `permission_handler` を使うので、`pubspec.yaml`に1行追加して`Pub get`を押して実行
```yaml
dartdependencies:
  flutter:
    sdk: flutter
  flutter_blue_plus: ^1.15.5
  permission_handler: ^11.0.1  # ★これを追加！
```
#### ４）BLEスキャンを行うアプリの基本画面を作成

その中に、位置情報パーミッション要求関数含めてonTapなど必要な要素を追加していく

###### 位置情報パーミッション要求関数
`main.dart`の`ScanScreen` クラスの中に、次のメソッドを追加
```dart
import 'package:permission_handler/permission_handler.dart'; // ←これもimport！

// 位置情報パーミッション要求関数
Future<void> requestPermissions() async {
  await Permission.location.request();
}
```
そして、`startScan()` の最初にこれを呼ぶ
```dart
void startScan() async {
  await requestPermissions();  // ★パーミッション要求を追加！

  scanResults.clear();
  FlutterBluePlus.startScan(timeout: const Duration(seconds: 4));
  FlutterBluePlus.scanResults.listen((results) {
    setState(() {
      scanResults = results;
    });
  });
}
```
ここまでを反映したmain.dartで、スマホからPicoWのBLE温度センサ「PicoTemp」が検出（スキャン）できることを確認。（この時点のmain.dart保存し忘れ）

その上で先に進む。
１．リストに表示されているPicoTempをタップする！
２．接続する！（BLE GATT接続）
３．LED ON / OFFボタンで制御する！

###### スキャンリストのデバイスをタップしたら接続する機能を追加
１．`ListTile` に `onTap:` を追加
２．タップすると `connect()` を呼び出す
３．接続できたらコンソールに「接続成功！」と表示

まずは、下記を`main.dart`の`ListTile`に追加。
```dart
return ListTile(
  title: Text(device.platformName.isNotEmpty ? device.platformName : "Unknown"),
  subtitle: Text(device.remoteId.str),
  onTap: () async {
    try {
      await device.connect();
      print("接続成功: ${device.platformName}");
      // ここにLED操作画面へ遷移するコードも後で追加するよ！
    } catch (e) {
      print("接続失敗: $e");
    }
  },
);
```
ここまでできたら、スマホで動作確認。AndroidStudioのコンソールに接続成功、と出たら次へ。
###### BLE接続したらLEDを制御する機能を追加
`main.dart`に、`LEDControlScreen`クラスを追加。
```dart
class LEDControlScreen extends StatelessWidget {
  final BluetoothDevice device;

  const LEDControlScreen({super.key, required this.device});

  Future<void> _sendCommand(String command) async {
    List<BluetoothService> services = await device.discoverServices();

    for (BluetoothService service in services) {
      for (BluetoothCharacteristic characteristic in service.characteristics) {
        if (characteristic.properties.write) {
          await characteristic.write(command.codeUnits);
          return;
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('LED Control'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => _sendCommand("ON"),
              child: const Text('LED ON'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => _sendCommand("OFF"),
              child: const Text('LED OFF'),
            ),
          ],
        ),
      ),
    );
  }
}
```
BLE接続したらLED制御画面を出す`onTap`を追加修正
```dart
onTap: () async {
  try {
    await device.connect();
    debugPrint("接続成功: ${device.platformName}");

    if (!mounted) return;  // ★これを追加！

    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => LEDControlScreen(device: device),
      ),
    );

  } catch (e) {
    debugPrint("接続失敗: $e");
  }
},
```

これらを踏まえて…
[前回、プロジェクトの新規作成で自動作成されたデモアプリ](https://qiita.com/NKJMeng/items/1c451bb3c07ba4b1e34b)の`lib/main.dart`ファイルの内容をいったん全部消して、次のコードを貼り付け。
 ```dart
 import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pico BLE App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ScanScreen(),
    );
  }
}

class ScanScreen extends StatefulWidget {
  const ScanScreen({super.key});

  @override
  State<ScanScreen> createState() => _ScanScreenState();
}

class _ScanScreenState extends State<ScanScreen> {
  List<ScanResult> scanResults = [];

  // 位置情報パーミッション要求関数
  Future<void> requestPermissions() async {
    await Permission.location.request();
  }

  void startScan() async {
    await requestPermissions();  // ★パーミッション要求を追加！

    scanResults.clear();
    FlutterBluePlus.startScan(timeout: const Duration(seconds: 4));
    FlutterBluePlus.scanResults.listen((results) {
      setState(() {
        scanResults = results;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BLEスキャンアプリ'),
      ),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: startScan,
            child: const Text('スキャン開始'),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: scanResults.length,
              itemBuilder: (context, index) {
                final device = scanResults[index].device;
                return ListTile(
                  title: Text(device.platformName.isNotEmpty ? device.platformName : "Unknown"),
                  subtitle: Text(device.remoteId.str),
                  onTap: () async {
                    try {
                      await device.connect();
                      debugPrint("接続成功: ${device.platformName}");

                      if (!mounted) return;  // ★これを追加！

                      // 接続成功したら画面遷移！
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => LEDControlScreen(device: device),
                        ),
                      );

                    } catch (e) {
                      debugPrint("接続失敗: $e");
                    }
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

class LEDControlScreen extends StatelessWidget {
  final BluetoothDevice device;

  const LEDControlScreen({super.key, required this.device});

  Future<void> _sendCommand(String command) async {
    List<BluetoothService> services = await device.discoverServices();

    for (BluetoothService service in services) {
      for (BluetoothCharacteristic characteristic in service.characteristics) {
        if (characteristic.properties.write) {
          await characteristic.write(command.codeUnits);
          return;
        }
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('LED Control'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => _sendCommand("ON"),
              child: const Text('LED ON'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => _sendCommand("OFF"),
              child: const Text('LED OFF'),
            ),
          ],
        ),
      ),
    );
  }
}
```

スマホから、PicoWの実装LEDを点灯／消灯の切り替えができることを確認。
写真やログ、各作業で出たエラーとその対処はのちほど追加するか別ページにまとめたい。
ひとまず、作業メモとして。