はじめに (対象読者・この記事でわかること)
この記事は、Flutter開発者で、Bluetooth Low Energy (BLE) 通信に興味がある方、特にスマートフォンをBLEのペリフェラル(周辺機器)として機能させたい方を対象としています。近年、IoTデバイスとの連携や、スマートフォン同士を簡易的なセンサーハブとして利用するニーズが増えており、BLEペリフェラルの実装スキルは非常に重要です。
この記事を読むことで、Flutter製のble_peripheralライブラリの基本的な使い方から、BLEのWrite機能を実装し、クライアント(セントラル機器)からのデータ書き込み要求に応答する具体的な手順を学ぶことができます。BLEペリフェラルとしてのアプリ開発の第一歩を踏み出し、よりインタラクティブなモバイルアプリケーションやIoTソリューションを構築できるようになるでしょう。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - Flutterの基本的な開発経験(Widget、StatefulWidget、非同期処理など) - Dart言語の基本的な知識 - Bluetooth Low Energy (BLE) の基本的な概念(セントラル、ペリフェラル、サービス、キャラクタリスティック、UUIDなど)
ble_peripheralライブラリの概要とBLE Writeの重要性
Flutterのble_peripheralライブラリは、FlutterアプリケーションをBluetooth Low Energy (BLE) のペリフェラルとして機能させるための強力なツールです。通常、スマートフォンはBLEのセントラル(中央機器)として動作し、他のペリフェラル(例:スマートウォッチ、IoTセンサー)をスキャン・接続・操作しますが、このライブラリを使うことで、スマートフォン自体がサービスを公開し、他のセントラルからの接続を受け入れることができるようになります。
BLE通信において、Write機能はセントラルからペリフェラルに対してデータを送信する主要な手段です。例えば、セントラルがペリフェラルの設定を変更したり、特定のコマンドを送信して動作を制御したりする場合に利用されます。具体的には、スマートフォンのアプリが温度センサーとして動作し、別のスマートフォンアプリから「測定間隔を変更する」といったコマンドを受け取る、といったシナリオでWrite機能が活躍します。ペリフェラル側がこのWrite要求を適切に処理し、必要に応じて応答を返すことで、双方向の柔軟な通信が実現可能になります。この機能は、IoTデバイスとの連携、データロガー、簡易的なプロトコル通信など、多岐にわたるアプリケーションで不可欠な要素となります。
BLEの基本的な用語を再確認しておきましょう。 - セントラル (Central): 他のデバイス(ペリフェラル)をスキャンし、接続するデバイス。 - ペリフェラル (Peripheral): サービスをアドバタイズし、セントラルからの接続を待つデバイス。 - サービス (Service): 関連するキャラクタリスティックの集合。通常、特定の機能を表します(例:心拍サービス、バッテリーサービス)。UUIDで識別されます。 - キャラクタリスティック (Characteristic): サービス内に含まれるデータ項目。実際のデータを保持し、Read/Write/Notifyなどの操作が可能です。UUIDで識別されます。 - UUID (Universally Unique Identifier): サービスやキャラクタリスティックを一意に識別するための128ビットの識別子。
Flutter ble_peripheralでBLE Writeを実装する手順
ここでは、ble_peripheralライブラリを使って、クライアントからのBLE Write要求を受け付けるペリフェラルをFlutterで実装する具体的な手順を解説します。
ステップ1: プロジェクトのセットアップとライブラリの追加
まず、新しいFlutterプロジェクトを作成するか、既存のプロジェクトを開きます。次に、pubspec.yamlファイルにble_peripheralライブラリを追加します。
Yamldependencies: flutter: sdk: flutter ble_peripheral: ^latest_version # 最新バージョンを指定
依存関係を追加したら、flutter pub getコマンドを実行します。
プラットフォーム固有の設定: BLE機能を利用するためには、AndroidとiOSそれぞれでパーミッションの設定が必要です。
Androidの場合:
android/app/src/main/AndroidManifest.xmlファイルに以下のパーミッションを追加します。
Xml<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <!-- Android 12 (API level 31) 以降で追加されたパーミッション --> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/> <application ... </application> </manifest>
Android 12以降では、より詳細なBluetoothパーミッションが必要になります。これらのパーミッションは、アプリの実行時にユーザーに許可を求める必要があります。
iOSの場合:
ios/Runner/Info.plistファイルに以下のキーを追加します。
Xml<key>NSBluetoothPeripheralUsageDescription</key> <string>This app uses Bluetooth to act as a peripheral device.</string> <key>NSBluetoothAlwaysUsageDescription</key> <string>This app uses Bluetooth to act as a peripheral device.</string>
NSBluetoothPeripheralUsageDescriptionはペリフェラルとして使用する場合の利用目的、NSBluetoothAlwaysUsageDescriptionはバックグラウンドでもBLEを使用する場合に必要です。
ステップ2: BLEサービスの定義とWriteキャラクタリスティックの設定
BLEペリフェラルとして機能させるには、まずサービスとキャラクタリスティックを定義する必要があります。今回はWrite機能に焦点を当てるため、書き込み可能なキャラクタリスティックを設定します。
Dartimport 'dart:typed_data'; import 'package:ble_peripheral/ble_peripheral.dart'; // BLEサービスとキャラクタリスティックのUUIDを定義します。 // これらのUUIDは、クライアント側と一致している必要があります。 // 実際には、カスタムUUIDジェネレータで生成した一意なUUIDを使用してください。 const String SERVICE_UUID = "602263C7-B9C6-4C23-AD1C-9E65E1CE264A"; const String WRITE_CHARACTERISTIC_UUID = "602263C7-B9C6-4C23-AD1C-9E65E1CE264B"; class BleServiceManager { final BlePeripheral _blePeripheral = BlePeripheral.instance; final List<String> _receivedDataHistory = []; // 受信したデータ履歴 // UIにデータを通知するためのコールバック Function(String)? onDataReceived; Function(String)? onStatusUpdate; Future<void> initializeBlePeripheral() async { onStatusUpdate?.call("Initializing BLE peripheral..."); try { // Android 12 (API level 31) 以降で必要なパーミッションを要求 // iOSはInfo.plistの設定で対応 if (Theme.of(WidgetsBinding.instance.window.physicalSize.width < 1000 ? TargetPlatform.android : TargetPlatform.iOS) == TargetPlatform.android) { final bool permissionsGranted = await _blePeripheral.requestBlePermissions(); if (!permissionsGranted) { onStatusUpdate?.call("Bluetooth permissions denied."); return; } } // Write可能なキャラクタリスティックを定義 final writeCharacteristic = Characteristic( uuid: WRITE_CHARACTERISTIC_UUID, properties: CharacteristicProperties.write, // このキャラクタリスティックが書き込み可能であることを示す permissions: CharacteristicPermissions.writeable, // 書き込み権限を設定 // クライアントからデータが書き込まれた際に呼ばれるコールバック onWrite: (value) async { final receivedString = String.fromCharCodes(value); // 受信したバイトデータを文字列に変換 print("Received data from client: $receivedString"); _receivedDataHistory.add("Received: $receivedString"); onDataReceived?.call(receivedString); // UIに通知 onStatusUpdate?.call("Data received: $receivedString"); return WriteResponse.success; // クライアントに書き込み成功を応答 }, ); // サービスを定義し、上記キャラクタリスティックを追加 final service = PeripheralService( uuid: SERVICE_UUID, characteristics: [writeCharacteristic], ); await _blePeripheral.addService(service); onStatusUpdate?.call("BLE Service added with UUID: $SERVICE_UUID"); } catch (e) { onStatusUpdate?.call("Error during BLE initialization: $e"); print("BLE initialization error: $e"); } } Future<void> startAdvertising() async { onStatusUpdate?.call("Starting advertising..."); try { await _blePeripheral.startAdvertising( request: AdvertiseRequest( serviceUuids: [SERVICE_UUID], // アドバタイズするサービスUUIDを指定 localName: "FlutterBLEWriter", // アドバタイズ名 ), ); onStatusUpdate?.call("Advertising started as 'FlutterBLEWriter'."); } catch (e) { onStatusUpdate?.call("Failed to start advertising: $e"); print("Failed to start advertising: $e"); } } Future<void> stopAdvertising() async { onStatusUpdate?.call("Stopping advertising..."); try { await _blePeripheral.stopAdvertising(); onStatusUpdate?.call("Advertising stopped."); } catch (e) { onStatusUpdate?.call("Failed to stop advertising: $e"); print("Failed to stop advertising: $e"); } } List<String> get receivedDataHistory => _receivedDataHistory; void dispose() { _blePeripheral.stopAdvertising(); // 必要であれば、_blePeripheral.removeService(serviceUuid); などでサービスを削除 } }
上記のコードでは、以下のことを行っています。
- SERVICE_UUIDとWRITE_CHARACTERISTIC_UUIDを定義。これらはクライアント側が認識するUUIDと一致している必要があります。
- Characteristicインスタンスを作成し、propertiesをCharacteristicProperties.writeに、permissionsをCharacteristicPermissions.writeableに設定することで、書き込み可能にします。
- 最も重要なのはonWriteコールバックです。クライアントからデータが書き込まれた際に、このコールバックが実行され、書き込まれたデータがvalueとしてUint8List形式で渡されます。
- PeripheralServiceにこのキャラクタリスティックを追加し、_blePeripheral.addService()でサービスをBLEスタックに登録します。
ステップ3: ペリフェラルアドバタイズの開始
サービスを定義したら、次にそのサービスを周囲のセントラルデバイスに通知するためにアドバタイズを開始します。
Dart// BleServiceManager クラスの startAdvertising メソッドを再度参照してください。 // Future<void> startAdvertising() async { // // ... 省略 ... // await _blePeripheral.startAdvertising( // request: AdvertiseRequest( // serviceUuids: [SERVICE_UUID], // アドバタイズするサービスUUIDを指定 // localName: "FlutterBLEWriter", // アドバタイズ名 // ), // ); // // ... 省略 ... // }
startAdvertisingメソッドでは、AdvertiseRequestオブジェクトを渡します。serviceUuidsに定義したサービスUUIDを含めることで、クライアントは特定サービスを持つペリフェラルとしてこのアプリを検出できるようになります。localNameは、クライアントがスキャンした際に表示されるデバイス名です。
ステップ4: UIの実装と状態管理
BleServiceManagerを使って、BLEペリフェラルの状態を表示し、アドバタイズの開始/停止を制御する簡単なUIを作成します。
Dartimport 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // SystemChrome for orientation // BleServiceManager クラスは前のステップで定義済みとします。 class BlePeripheralScreen extends StatefulWidget { const BlePeripheralScreen({super.key}); @override State<BlePeripheralScreen> createState() => _BlePeripheralScreenState(); } class _BlePeripheralScreenState extends State<BlePeripheralScreen> { final BleServiceManager _bleService = BleServiceManager(); bool _isAdvertising = false; String _statusMessage = "Ready"; final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); // 画面の向きを縦に固定 (任意) SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); _bleService.onDataReceived = (data) { setState(() { // UIが更新されたらリストの最下部にスクロール WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); }); }); }; _bleService.onStatusUpdate = (message) { setState(() { _statusMessage = message; }); }; _bleService.initializeBlePeripheral(); } @override void dispose() { _bleService.dispose(); SystemChrome.setPreferredOrientations(DeviceOrientation.values); // 向きの固定を解除 _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Flutter BLE Write Peripheral"), elevation: 4, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Card( elevation: 2, child: Padding( padding: const EdgeInsets.all(12.0), child: Text( "Status: $_statusMessage", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), ), const SizedBox(height: 20), ElevatedButton( onPressed: () async { if (_isAdvertising) { await _bleService.stopAdvertising(); } else { await _bleService.startAdvertising(); } setState(() { _isAdvertising = !_isAdvertising; }); }, style: ElevatedButton.styleFrom( backgroundColor: _isAdvertising ? Colors.redAccent : Colors.green, padding: const EdgeInsets.symmetric(vertical: 15), textStyle: const TextStyle(fontSize: 18), ), child: Text(_isAdvertising ? "Stop Advertising" : "Start Advertising"), ), const SizedBox(height: 30), const Text( "Received Data History:", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const Divider(), Expanded( child: _bleService.receivedDataHistory.isEmpty ? const Center( child: Text( "No data received yet. Start advertising and connect with a BLE central device.", textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), ) : ListView.builder( controller: _scrollController, itemCount: _bleService.receivedDataHistory.length, itemBuilder: (context, index) { return Card( margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 0), elevation: 1, child: ListTile( leading: const Icon(Icons.arrow_downward, color: Colors.blueAccent), title: Text( _bleService.receivedDataHistory[index], style: const TextStyle(fontSize: 16), ), ), ); }, ), ), ], ), ), ); } }
このUIでは、アドバタイズの状態と受信したデータをリアルタイムで表示します。_bleService.onDataReceivedコールバックを通じて、データが受信されるたびにUIが更新され、履歴として表示されます。
ハマった点やエラー解決
ble_peripheralライブラリを使用する際やBLE開発全般で遭遇しやすい問題と、その解決策について解説します。
-
Bluetoothが有効になっていない、またはパーミッションが不足している:
- 現象:
initializeBlePeripheralやstartAdvertisingがエラーを返す、または何も反応しない。 - 解決策:
- スマートフォンのBluetooth設定がオンになっているか確認します。
- Androidの場合、アプリの初回起動時に表示されるBluetooth関連のパーミッション要求を許可しているか確認します。特にAndroid 12以降では、
BLUETOOTH_ADVERTISE,BLUETOOTH_CONNECT,BLUETOOTH_SCANの3つのパーミッションが必要です。アプリの設定画面から手動で許可することもできます。 - iOSの場合、
Info.plistにNSBluetoothPeripheralUsageDescriptionとNSBluetoothAlwaysUsageDescriptionが正しく設定されているか確認します。
- 現象:
-
クライアントからペリフェラルが見つからない:
- 現象: セントラルアプリ(例: BLE Scannerアプリ)でFlutterアプリが見つからない。
- 解決策:
startAdvertisingが正しく呼ばれているか確認します。AdvertiseRequestのserviceUuidsに指定したUUIDが、PeripheralServiceに登録したUUIDと一致しているか、またクライアントがスキャンしているUUIDと一致しているか確認します。localNameは表示されますが、サービスUUIDでフィルタリングしないと見つからない場合があります。- 他のBLEアプリがバックグラウンドでBluetoothを使っている場合、競合してアドバタイズが開始できないことがあります。他のアプリを終了してみてください。
- UUIDは128ビットの正しいフォーマット(例:
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)で記述されているか確認します。
-
クライアントからデータが書き込めない/
onWriteコールバックが呼ばれない:- 現象: クライアントからは書き込みに成功したように見えるが、Flutterアプリの
onWriteが実行されない。 - 解決策:
CharacteristicのpropertiesにCharacteristicProperties.writeが含まれているか確認します。writeWithoutResponseも受け付ける場合はCharacteristicProperties.writeWithoutResponseも追加します。permissionsにCharacteristicPermissions.writeableが設定されているか確認します。- クライアントが正しいサービスUUIDとキャラクタリスティックUUIDに対して書き込みを行っているか確認します。
- クライアントが接続してから書き込みを行っているか確認します。アドバタイズ状態では書き込みはできません。
- 現象: クライアントからは書き込みに成功したように見えるが、Flutterアプリの
-
iOSでのバックグラウンドアドバタイズの制限:
- 現象: iOSでは、アプリがバックグラウンドに移行するとBLEアドバタイズが停止したり、アドバタイズの間隔が長くなったりする。
- 解決策: iOSのBLEは省電力のため、バックグラウンドでの動作に制限があります。アプリがフォアグラウンドにある間のみアドバタイズを期待するのが一般的です。もしバックグラウンドでの動作が必要な場合は、
Info.plistにUIBackgroundModesキーを設定し、bluetooth-peripheralを追加するなど、追加の設定と考慮が必要です。ただし、完全に停止させずに永続的にバックグラウンドアドバタイズすることは困難です。
これらのトラブルシューティングのヒントは、開発プロセスにおいて時間を節約するのに役立つでしょう。
まとめ
本記事では、Flutterのble_peripheralライブラリを使用して、FlutterアプリケーションをBLEペリフェラルとして機能させ、クライアントからのデータ書き込み(Write)要求を受け付けて応答するまでの具体的な手順を解説しました。
- BLEペリフェラル化:
ble_peripheralライブラリを導入し、FlutterアプリをBLEペリフェラルとして設定する方法を学びました。 - Write機能の実装: サービスと書き込み可能なキャラクタリスティックを定義し、
onWriteコールバックを使ってクライアントからのデータを受信・処理する方法を習得しました。 - プラットフォーム設定とトラブルシューティング: Android/iOSそれぞれのパーミッション設定や、開発中によく遭遇する問題点とその解決策について詳しく解説しました。
この記事を通して、スマートフォンをBLEペリフェラルとして活用し、IoTデバイスとの連携や他のモバイルアプリとの双方向通信を実現するための基礎的な知識と実践的なスキルが得られたことと思います。この知識を基に、より高度なBLE通信やIoTソリューションの開発に挑戦してみてください。
今後は、ReadやNotify機能の実装、複数のサービスやキャラクタリスティックの管理、セキュリティを考慮したBLE通信、そしてより複雑なBLEプロトコルの構築など、発展的な内容についても記事にする予定です。
参考資料
- ble_peripheral パッケージ - pub.dev
- Flutter 公式サイト
- Bluetooth Low Energy (BLE) の基本概念 - Qiita (BLEの一般的な概念理解に役立ちます)
