はじめに
以下の記事の続きにあたる内容です。
- micro:bit CreateAI で BLE通信を使ってみる(ブラウザの Web Bluetooth API との組み合わせで) - Qiita
- Flutterアプリ(flutter_blue_plus の Android用サンプル)と micro:bit CreateAI のプログラムを BLE通信でつないでみる - Qiita
- flutter_blue_plus を使った BLEスキャンを Flutter で実装してみる: Android Studio で Androidアプリをビルド - Qiita
今回やること
今回やる内容は、flutter_blue_plus を使った「BLE による Flutterアプリと micro:bit の接続/切断」です。
Android Studio で Androidアプリをビルドし、Androidスマホで動作させます。
実装
それでは早速、実装を進めます。
前回までの実装内容
冒頭に掲載していた前回記事の続きで進めていきます。
前回記事では、パーミッションの追加などの下準備をして、Flutterアプリからデバイスのスキャンを行うところまでやっていました。
接続処理の実装
スキャンの処理までは試していたので、さらに以下の公式ページに書かれた接続処理を加えます。
●flutter_blue_plus | Flutter package
https://pub.dev/packages/flutter_blue_plus
前回記事での実装では、スキャンした結果をアプリ上に表示していましたが、今回はその表示は行いません。
今回は「デバイス名に BBC micro:bit が含まれるデバイス」に自動で接続することにします(いったん仮の想定として、自分が用意した micro:bit が 1台だけある状況とします)。
なお、micro:bit は、「こちらの記事で実装していた BLE UART の処理など」が書き込まれたものです。
前提の補足: パーミッションの追加など
Androidアプリで BLE を使うにはパーミッションの追加などが必要です。
それについては、前回の記事をご参照ください。
コード
以下が、接続処理を実装したものです。
スキャンを実装していた際に、スキャン実行のボタンだったものが、「スキャン + micro:bit への接続」を行うものになっています。
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),
),
);
}
}
基本的に、前回の実装に手を加えていった形です。
切断処理の追加
続いて、切断の処理も追加します。
追加するのは、「切断処理を実行するためのボタン」と「切断処理」です。
それらを追加したコードが以下になります。
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表示が変化しているのが分かります。
micro:bit の表示が変わっているのは、冒頭に掲載した記事内で書いているとおり、以下の実装をしているためです。
おわりに
Flutterアプリの実装で、flutter_blue_plus を使った「スキャン」⇒「micro:bit との接続/切断」の実装を試していきました。
あとは、micro:bit と接続が行われた後、以下の部分で実装している BLE UART のデータの受け取りが行えればと思います。
【追記】 BLE UART の受信処理も追加できました
BLE UART の受信処理も追加できました!