環境構築は前回の記事を参照
スマホ+BLEの基本から確認したいので各要素は次の通り
要素 | 内容 |
---|---|
画面 | 1画面だけ |
ボタン1 | LED ON(Picoにコマンド送信) |
ボタン2 | LED OFF(Picoにコマンド送信) |
BLEスキャン | ボタンを押すと、周辺のBLEデバイスを探してPicoに接続する |
通信方式 | GATT接続、CharacteristicにWrite |
スマホアプリの動作(まずはシンプルに)
├ スマホから周辺のBLEデバイスをスキャンする
├ PicoTempを見つけてリストに表示する
└ タップすると接続する
作業の流れ
1.必要なFlutterライブラリ(BLE通信用)を追加する
2.アプリ画面に「LED ON」「LED OFF」ボタンを設置する
3.BLEでPicoに接続してコマンドを送る
1.BLE通信ライブラリを追加する
使うライブラリはflutter_blue_plus
1)左のタブのProjectツリーから'pubspec.yaml'を探してダブルクリックで開く
2)pubspec.yaml
を編集する
「dependencies:」内に flutter_blue_plus: ^1.15.5
を追加。
この時、インデントもきちんと再現する。TABは不可。半角スペース2個。
dependencies:
flutter:
sdk: flutter
flutter_blue_plus: ^1.15.5 # ←追加した行
3)ctrl+S
でファイルの保存
4)カレントファイルの上部バー付近に出るPub get
ボタンを押す
ログにパッケージ依存関係の変更などが表示される。主な内容は次の通り。
メッセージ | 意味 | 今回どうするか |
---|---|---|
Changed 15 dependencies! | 15個のパッケージの依存関係が変更されました(正常) | ✅ 気にしなくていい |
9 packages have newer versions... | 9個のパッケージに新しいバージョンがあるけど今は使わないよ、という警告 | ✅ 今は気にしなくていい |
Try flutter pub outdated | もっと知りたかったらコマンドで調べてねって提案してるだけ | (興味あれば後で調べてもOK) |
Process finished with exit code 0 | 正常終了(エラーなし) | ✅ 問題なし |
問題ないので次へ進める。
2.双方とも、BLEスキャンができる設定にする
《AndroidのBLE仕様(Android 6.0以降)について》
・BLE電波からざっくりした位置情報が推測できてしまうことを懸念し、プライバシー保護のためにBLEスキャンをするには位置情報サービスもONにしていないと許可されないというルールがある。
なので、BluetoothだけONにしててもスキャンは動かない。
➡ 1)スマホの位置情報サービスを有効化
➡ 2)アプリの位置情報パーミッションを設定
また、単にManifestに書くだけじゃなくアプリ起動中に、ユーザーに「このアプリに位置情報アクセスを許可しますか?」と尋ねて、許可をもらう必要がある
➡ 3)「ランタイムパーミッション要求」の追加
これらが必要なので、位置情報をスキャンできるように次の設定変更を行う
1)スマホ設定の、位置情報サービス
をON
2)アプリ自体に位置情報パーミッションを設定
AndroidManifest.xml
を編集してパーミッション追加する。
まず、Android Studioの左カラムからandroid/app/src/main/AndroidManifest.xml
を開く
pico_temp_ble
├─ android
├─ app
├─ src
├─ main
└─ AndroidManifest.xml ←これ!!
<application>
タグより前(つまり <manifest>
の直後あたり)に、次の行を追記
<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" />
3)「ランタイムパーミッション要求」の追加
新しくライブラリ permission_handler
を使うので、pubspec.yaml
に1行追加してPub get
を押して実行
dartdependencies:
flutter:
sdk: flutter
flutter_blue_plus: ^1.15.5
permission_handler: ^11.0.1 # ★これを追加!
4)BLEスキャンを行うアプリの基本画面を作成
その中に、位置情報パーミッション要求関数含めてonTapなど必要な要素を追加していく
位置情報パーミッション要求関数
main.dart
のScanScreen
クラスの中に、次のメソッドを追加
import 'package:permission_handler/permission_handler.dart'; // ←これもimport!
// 位置情報パーミッション要求関数
Future<void> requestPermissions() async {
await Permission.location.request();
}
そして、startScan()
の最初にこれを呼ぶ
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保存し忘れ)
その上で先に進む。
1.リストに表示されているPicoTempをタップする!
2.接続する!(BLE GATT接続)
3.LED ON / OFFボタンで制御する!
スキャンリストのデバイスをタップしたら接続する機能を追加
1.ListTile
に onTap:
を追加
2.タップすると connect()
を呼び出す
3.接続できたらコンソールに「接続成功!」と表示
まずは、下記をmain.dart
のListTile
に追加。
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
クラスを追加。
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
を追加修正
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");
}
},
これらを踏まえて…
前回、プロジェクトの新規作成で自動作成されたデモアプリのlib/main.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を点灯/消灯の切り替えができることを確認。
写真やログ、各作業で出たエラーとその対処はのちほど追加するか別ページにまとめたい。
ひとまず、作業メモとして。