flutter_blue_plus を使った「BLEスキャン＋ micro:bit への接続／切断」を Flutter で実装してみる： Android Studio で Androidアプリをビルド

## はじめに

以下の記事の続きにあたる内容です。

- [micro:bit CreateAI で BLE通信を使ってみる（ブラウザの Web Bluetooth API との組み合わせで） - Qiita](https://qiita.com/youtoy/items/1b47551f79bc3e3aa50a)
- [Flutterアプリ（flutter_blue_plus の Android用サンプル）と micro:bit CreateAI のプログラムを BLE通信でつないでみる - Qiita](https://qiita.com/youtoy/items/b8e14601ecab557d3f72)
- [flutter_blue_plus を使った BLEスキャンを Flutter で実装してみる： Android Studio で Androidアプリをビルド - Qiita](https://qiita.com/youtoy/items/66b0563005c62b926ce1)


### 今回やること

今回やる内容は、flutter_blue_plus を使った「BLE による Flutterアプリと micro:bit　の接続／切断」です。

Android Studio で Androidアプリをビルドし、Androidスマホで動作させます。

## 実装

それでは早速、実装を進めます。

### 前回までの実装内容

冒頭に掲載していた[前回記事](https://qiita.com/youtoy/items/66b0563005c62b926ce1)の続きで進めていきます。

[前回記事](https://qiita.com/youtoy/items/66b0563005c62b926ce1)では、パーミッションの追加などの下準備をして、Flutterアプリからデバイスのスキャンを行うところまでやっていました。


### 接続処理の実装

スキャンの処理までは試していたので、さらに以下の公式ページに書かれた接続処理を加えます。

●flutter_blue_plus | Flutter package
　https://pub.dev/packages/flutter_blue_plus

![2025-03-27_22-00-57.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/11edfcd5-8ae2-4b70-b37c-67af0b4180a3.jpeg)

前回記事での実装では、スキャンした結果をアプリ上に表示していましたが、今回はその表示は行いません。

今回は「デバイス名に BBC micro:bit が含まれるデバイス」に自動で接続することにします（いったん仮の想定として、自分が用意した micro:bit が 1台だけある状況とします）。

なお、micro:bit は、「[こちらの記事](https://qiita.com/youtoy/items/1b47551f79bc3e3aa50a)で実装していた BLE UART の処理など」が書き込まれたものです。

#### 前提の補足： パーミッションの追加など


Androidアプリで BLE を使うにはパーミッションの追加などが必要です。

それについては、[前回の記事](https://qiita.com/youtoy/items/66b0563005c62b926ce1#%E3%83%91%E3%83%BC%E3%83%9F%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%BF%BD%E5%8A%A0)をご参照ください。

#### コード

以下が、接続処理を実装したものです。

スキャンを実装していた際に、スキャン実行のボタンだったものが、「スキャン ＋ micro:bit への接続」を行うものになっています。


```dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BLE Auto Connect Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'BLE Auto Connect Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  StreamSubscription<List<ScanResult>>? _scanSubscription;
  BluetoothDevice? _targetDevice;
  String _statusMessage = 'スキャン開始ボタンを押してください';

  Future<void> _startScan() async {
    // スキャン開始前に状態をリセット
    setState(() {
      _statusMessage = 'スキャン中...';
      _targetDevice = null;
    });

    // BluetoothがONになるのを待つ
    await FlutterBluePlus.adapterState.firstWhere(
        (state) => state == BluetoothAdapterState.on);

    bool foundDevice = false;
    _scanSubscription = FlutterBluePlus.onScanResults.listen(
      (results) async {
        for (var result in results) {
          final deviceName = result.advertisementData.advName;
          // スキャンしたデバイスのデバイス名が「BBC micro:bit」で始まるかを確認
          if (deviceName.startsWith("BBC micro:bit")) {
            if (!foundDevice) {
              foundDevice = true;
              // デバイスが見つかったらスキャンは停止
              _scanSubscription?.cancel();
              setState(() {
                _statusMessage = 'デバイス見つかりました: $deviceName\n接続中...';
              });
              BluetoothDevice device = result.device;
              _targetDevice = device;
              try {
                // 自動で接続
                await device.connect(autoConnect: true, mtu: null);
                await device.connectionState
                    .where((state) =>
                        state == BluetoothConnectionState.connected)
                    .first;
                setState(() {
                  _statusMessage = 'デバイスに接続完了: $deviceName';
                });
              } catch (e) {
                setState(() {
                  _statusMessage = '接続エラー: $e';
                });
              }
              break;
            }
          }
        }
      },
      onError: (e) {
        setState(() {
          _statusMessage = 'スキャンエラー: $e';
        });
      },
    );

    // スキャンのタイムアウトは 15秒に設定
    await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
    await FlutterBluePlus.isScanning
        .firstWhere((isScanning) => isScanning == false);
    if (!foundDevice) {
      setState(() {
        _statusMessage = 'BBC micro:bit が見つかりませんでした';
      });
    }
  }

  @override
  void dispose() {
    _scanSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Text(
          _statusMessage,
          textAlign: TextAlign.center,
          style: const TextStyle(fontSize: 18),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _startScan,
        tooltip: 'BLEスキャン',
        child: const Icon(Icons.search),
      ),
    );
  }
}
```

基本的に、[前回の実装](https://qiita.com/youtoy/items/66b0563005c62b926ce1#%E3%82%B3%E3%83%BC%E3%83%89)に手を加えていった形です。


#### 切断処理の追加

続いて、切断の処理も追加します。
追加するのは、「切断処理を実行するためのボタン」と「切断処理」です。

それらを追加したコードが以下になります。

```dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BLE Auto Connect Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'BLE Auto Connect Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  StreamSubscription<List<ScanResult>>? _scanSubscription;
  BluetoothDevice? _targetDevice;
  String _statusMessage = 'スキャン開始ボタンを押してください';

  // スキャン開始と micro:bit への自動接続
  Future<void> _startScan() async {
    setState(() {
      _statusMessage = 'スキャン中...';
      _targetDevice = null;
    });

    await FlutterBluePlus.adapterState.firstWhere(
        (state) => state == BluetoothAdapterState.on);

    bool foundDevice = false;
    _scanSubscription = FlutterBluePlus.onScanResults.listen(
      (results) async {
        for (var result in results) {
          final deviceName = result.advertisementData.advName;
          if (deviceName.startsWith("BBC micro:bit")) {
            if (!foundDevice) {
              foundDevice = true;
              _scanSubscription?.cancel();
              setState(() {
                _statusMessage = 'デバイス見つかりました: $deviceName\n接続中...';
              });
              BluetoothDevice device = result.device;
              _targetDevice = device;
              try {
                // autoConnect有効で接続（mtuはnull）
                await device.connect(autoConnect: true, mtu: null);
                // 接続状態が「connected」になるのを待機
                await device.connectionState
                    .where((state) =>
                        state == BluetoothConnectionState.connected)
                    .first;
                setState(() {
                  _statusMessage = 'デバイスに接続完了: $deviceName';
                });
              } catch (e) {
                setState(() {
                  _statusMessage = '接続エラー: $e';
                });
              }
              break;
            }
          }
        }
      },
      onError: (e) {
        setState(() {
          _statusMessage = 'スキャンエラー: $e';
        });
      },
    );

    // タイムアウト15秒でスキャンを実行
    await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
    await FlutterBluePlus.isScanning
        .firstWhere((isScanning) => isScanning == false);
    if (!foundDevice) {
      setState(() {
        _statusMessage = 'BBC micro:bit が見つかりませんでした';
      });
    }
  }

  // デバイスの切断処理
  Future<void> _disconnectDevice() async {
    if (_targetDevice != null) {
      try {
        await _targetDevice!.disconnect();
        setState(() {
          _statusMessage = 'デバイスとの接続を切断しました';
          _targetDevice = null;
        });
      } catch (e) {
        setState(() {
          _statusMessage = '切断エラー: $e';
        });
      }
    } else {
      setState(() {
        _statusMessage = '接続中のデバイスはありません';
      });
    }
  }

  @override
  void dispose() {
    _scanSubscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Text(
          _statusMessage,
          textAlign: TextAlign.center,
          style: const TextStyle(fontSize: 18),
        ),
      ),
      // 切断用のボタンを追加（左側がスキャン用、右側が切断用のボタン）
      floatingActionButton: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            heroTag: 'scan',
            onPressed: _startScan,
            tooltip: 'BLEスキャン',
            child: const Icon(Icons.search),
          ),
          const SizedBox(width: 16),
          FloatingActionButton(
            heroTag: 'disconnect',
            onPressed: _disconnectDevice,
            tooltip: '切断',
            child: const Icon(Icons.link_off),
          ),
        ],
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
    );
  }
}
```

#### アプリの実行結果

アプリを実行した結果は、以下のとおりです。

Flutterアプリの説続ボタン・切断ボタンを押した後、micro:bit側の LED表示が変化しているのが分かります。

<iframe width="560" height="315" src="https://www.youtube.com/embed/WoU4cBLyszU?si=jOZgg3CVzDpI09WC" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

micro:bit の表示が変わっているのは、[冒頭に掲載した記事内](https://qiita.com/youtoy/items/1b47551f79bc3e3aa50a#makecode%E5%81%B4%E3%81%A7%E3%81%AE%E8%BF%BD%E5%8A%A0%E5%AE%9F%E8%A3%85%E6%BA%96%E5%82%99)で書いているとおり、以下の実装をしているためです。

![2025-03-27_22-15-21.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/be40aecd-919f-4590-84cf-b08cd19dbef7.jpeg)


## おわりに

Flutterアプリの実装で、flutter_blue_plus を使った「スキャン」⇒「micro:bit との接続／切断」の実装を試していきました。

あとは、micro:bit と接続が行われた後、以下の部分で実装している BLE UART のデータの受け取りが行えればと思います。

![2025-03-27_22-17-30.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/4d3ad63b-ab02-43a1-aaac-3b71dade31c1.jpeg)


### 【追記】 BLE UART の受信処理も追加できました

BLE UART の受信処理も追加できました！

<iframe width="560" height="315" src="https://www.youtube.com/embed/7Nq0b9ifP60?si=B_ErRYNceYksrWOr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

#### 【追記2】 関連記事

↓こちらの記事も書いてみました。

●flutter_blue_plus を使って「micro:bit からのデータ受信（BLE UART）」を Flutter で実装してみる： Android Studio で Androidアプリをビルド - Qiita
　https://qiita.com/youtoy/items/a29902cb283f97f12912