はじめに
以下の記事の続きにあたる内容で、flutter_blue_plus を使って「micro:bit からのデータ受信(BLE UART)」を Flutter で実装してみたという話です。
Flutter で BLE を扱う
直近の記事とその時の動作結果
この記事の内容を実装してみる前に、直近では flutter_blue_plus を使って Androidアプリと micro:bit との BLE での接続/切断を試してみていました。
そして、以下の記事を書いたりしていました。
●flutter_blue_plus を使った「BLEスキャン+ micro:bit への接続/切断」を Flutter で実装してみる: Android Studio で Androidアプリをビルド - Qiita
https://qiita.com/youtoy/items/c5b70869f152ca5d29f5
それを動作させた時の様子は、↓こんな感じです。
上記の前の流れ
さらにその前には、micro:bit でデバイスの動きを AI で学習させたり、さらに micro:bit で BLE通信を実装したり、Flutterアプリで BLE通信を扱うための下準備などを行ったりしていました。
それらについて、以下の記事を書いていました。
- 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 UART によるデータ受信」を実装してみるのですが、その際、これまでも使ってきた「micro:bit」での「micro:bit からのデータ送信(BLE UART)」を Flutterアプリ(Androidアプリ)で受信できるようにします。
micro:bit の実装内容については、過去の記事をご参照ください。
今回の内容を使ったデモ
それが動いている時の様子は以下のとおりです。
micro:bit との接続後、micro:bit を「真横に揺らす」「バタバタさせる」という 2種類の動きをさせた時に、画面内の中央の少し下あたりで、数字の 1 や 2 が表示されます(画面上の文字が小さくて、すごく分かりにくいですが...)。
数字の表示されている部分をピックアップしてみました。
前に書いた記事の中の、micro:bit での実装で言うと、以下に該当する部分のデータを受信した結果の表示になります。
実際に試していく
実際に実装を試していきます。
下準備
今回も Androidアプリで BLE を使うので、パーミッションの追加が必要です。
以下、前に書いた記事からの再掲です。
パーミッションの追加(再掲)
「android/app/src/main/AndroidManifest.xml」にパーミッションを追加します。
追加する内容は、flutter_blue_plus の公式ページの以下に書かれているものです。
●flutter_blue_plus | Flutter package
https://pub.dev/packages/flutter_blue_plus#add-permissions-for-android-no-location
自分のスマホは Android 15 なので、以下の部分だけ追記します。
<!-- Tell Google Play Store that your app uses Bluetooth LE
Set android:required="true" if bluetooth is necessary -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
追記後の冒頭部分は、以下のとおりです。
パッケージの追加(再掲)
こちらも前の記事の再掲になりますが、パッケージの追加についてです。
以下のコマンドで、flutter_blue_plus をインストールします(自分の環境は FVM を使っているので、先頭部分に fvm をつけています)。
fvm flutter pub add flutter_blue_plus
BLE UART の処理を実装
BLE UART の処理を実装していきます。
コードは、前回の記事のものをベースに、付け加えていきます。
実装内容
実装したコードを掲載します。
BLE UART周りの処理などは、冒頭に掲載していた記事で扱っていたもの(ブラウザの Web Bluetooth API を使った受信処理)を、Flutter で実装する形にしてみました。
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から受信した内容を扱うリスト
List<String> _receivedMessages = [];
// BLE UART サービスとキャラクタリスティックの UUID
static const String UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
static const String UART_TX_CHAR_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
// スキャンと自動接続
Future<void> _startScan() async {
setState(() {
_statusMessage = 'スキャン中...';
_targetDevice = null;
_receivedMessages.clear();
});
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';
});
// 接続後にサービスを探す処理
List<BluetoothService> services = await device.discoverServices();
BluetoothService? uartService;
for (BluetoothService service in services) {
if (service.uuid.toString().toLowerCase() == UART_SERVICE_UUID) {
uartService = service;
break;
}
}
if (uartService != null) {
// 各キャラクタリスティックで読み取り可能なものを read()
for (BluetoothCharacteristic c in uartService.characteristics) {
if (c.properties.read) {
try {
List<int> value = await c.read();
print("Characteristic ${c.uuid}: $value");
} catch (e) {
print("Characteristic ${c.uuid} の読み取りエラー: $e");
}
}
}
// TX キャラクタリスティックを取得して通知を開始する
BluetoothCharacteristic? txCharacteristic;
for (BluetoothCharacteristic c in uartService.characteristics) {
if (c.uuid.toString().toLowerCase() == UART_TX_CHAR_UUID) {
txCharacteristic = c;
break;
}
}
if (txCharacteristic != null) {
await txCharacteristic.setNotifyValue(true);
txCharacteristic.value.listen((value) {
// 受信したバイト列を文字列に変換し、念のため改行コードを除去
String received =
String.fromCharCodes(value).replaceAll('\n', '');
setState(() {
_receivedMessages.add(received);
});
});
} else {
setState(() {
_statusMessage = "UART TX キャラクタリスティックが見つかりませんでした";
});
}
} else {
setState(() {
_statusMessage = "UART サービスが見つかりませんでした";
});
}
} 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;
_receivedMessages.clear();
});
} 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: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_statusMessage,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
const Text(
"受信データ:",
style: TextStyle(fontWeight: FontWeight.bold),
),
// 受信した各メッセージを表示。内容が "1" の場合は文字色を赤に設定
Column(
children: _receivedMessages.map((msg) {
return Text(
msg,
style: TextStyle(
color: msg.trim() == "1" ? Colors.red : Colors.black,
),
);
}).toList(),
),
],
),
),
),
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,
);
}
}
これで動作確認をしてみたところ、切断後に再接続すると、BLE UART でのデータ受け取りが 2重になりました。
Flutterアプリと micro:bit との切断時に、BLE UART でのデータ受け取りが維持されたままになったようです。
修正した実装内容
上記の問題に対応するため、コードを以下のように修正しました。
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;
StreamSubscription<List<int>>? _txSubscription;
// 接続対象のデバイス
BluetoothDevice? _targetDevice;
// TXキャラクタリスティックの参照を保持
BluetoothCharacteristic? _txCharacteristic;
String _statusMessage = 'スキャン開始ボタンを押してください';
// micro:bitから受信したデータを扱うリスト
List<String> _receivedMessages = [];
// BLE UART サービスとキャラクタリスティックの UUID
static const String UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
static const String UART_TX_CHAR_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
// スキャンと自動接続
Future<void> _startScan() async {
setState(() {
_statusMessage = 'スキャン中...';
_targetDevice = null;
_receivedMessages.clear();
});
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';
});
// 接続後にサービスを探す処理
List<BluetoothService> services = await device.discoverServices();
BluetoothService? uartService;
for (BluetoothService service in services) {
if (service.uuid.toString().toLowerCase() ==
UART_SERVICE_UUID) {
uartService = service;
break;
}
}
if (uartService != null) {
// 各キャラクタリスティックで読み取り可能なものを read()(必要に応じて)
for (BluetoothCharacteristic c in uartService.characteristics) {
if (c.properties.read) {
try {
List<int> value = await c.read();
print("Characteristic ${c.uuid}: $value");
} catch (e) {
print("Characteristic ${c.uuid} の読み取りエラー: $e");
}
}
}
// TX キャラクタリスティックを取得して通知を開始する
for (BluetoothCharacteristic c in uartService.characteristics) {
if (c.uuid.toString().toLowerCase() ==
UART_TX_CHAR_UUID) {
_txCharacteristic = c;
break;
}
}
if (_txCharacteristic != null) {
await _txCharacteristic!.setNotifyValue(true);
// 既存のSubscriptionがある場合はキャンセルする
await _txSubscription?.cancel();
_txSubscription = _txCharacteristic!.value.listen((value) {
// 受信したバイト列を文字列に変換し、改行コードを除去
String received =
String.fromCharCodes(value).replaceAll('\n', '');
setState(() {
_receivedMessages.add(received);
});
});
} else {
setState(() {
_statusMessage =
"UART TX キャラクタリスティックが見つかりませんでした";
});
}
} else {
setState(() {
_statusMessage = "UART サービスが見つかりませんでした";
});
}
} 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 {
// 通知のSubscriptionがある場合はキャンセルして通知も解除
if (_txSubscription != null) {
await _txSubscription!.cancel();
_txSubscription = null;
}
if (_txCharacteristic != null) {
await _txCharacteristic!.setNotifyValue(false);
}
await _targetDevice!.disconnect();
setState(() {
_statusMessage = 'デバイスとの接続を切断しました';
_targetDevice = null;
_receivedMessages.clear();
});
} catch (e) {
setState(() {
_statusMessage = '切断エラー: $e';
});
}
} else {
setState(() {
_statusMessage = '接続中のデバイスはありません';
});
}
}
@override
void dispose() {
_scanSubscription?.cancel();
_txSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_statusMessage,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
const Text(
"受信データ:",
style: TextStyle(fontWeight: FontWeight.bold),
),
// 受信した各メッセージを表示。内容が "1" の場合は文字色を赤に設定
Column(
children: _receivedMessages.map((msg) {
return Text(
msg,
style: TextStyle(
color: msg.trim() == "1" ? Colors.red : Colors.black,
),
);
}).toList(),
),
],
),
),
),
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,
);
}
}
あらためて動作確認を行ったところ、問題は解消しました。
【余談】 少し見やすくしてみたもの
上記を動作させた結果は、冒頭に掲載していた動画のとおりになります。
しかし、その動画でスマホ画面上の変化が分かりにくかったので、背景色を変化させるなどして分かりやすくしてみました。
変更した内容と実行結果
具体的には、micro:bit との接続時や、micro:bit を「真横に揺らす」「バタバタさせる」という 2種類の動きをさせた時に、アプリの背景色がそれぞれ異なる色に変わるような実装を試しています(それと、画面上に表示されているテキストの内容も変わります)。
それを動作させた時の様子は、以下のようになりました。