0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スマホとPicoWをBLE接続する簡単なアプリを作る

Posted at

環境構築は前回の記事を参照

スマホ+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'を探してダブルクリックで開く
image.png
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" />

image.png

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.dartScanScreen クラスの中に、次のメソッドを追加

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.ListTileonTap: を追加
2.タップすると connect() を呼び出す
3.接続できたらコンソールに「接続成功!」と表示

まずは、下記をmain.dartListTileに追加。

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を点灯/消灯の切り替えができることを確認。
写真やログ、各作業で出たエラーとその対処はのちほど追加するか別ページにまとめたい。
ひとまず、作業メモとして。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?