본문 바로가기

개발 Note/Dart,Flutter

[Flutter] Bluetooth, BLE scanning

반응형

요즘 Flutter 로 ble device를 다루는 작업을 하고 있습니다.

 

아시다 시피 BLE 장치들은 기존 BT classic과 다르게 broadcasting 기능을 제공하고 있어서 S/W로직을 기존 BT classic과 동일하게 가져가기에는 무리가 있습니다.

BLE 장치를 어떤 목적으로 어떻게 사용할것인가를 잘 구상 해야합니다.

구성 하고자 하는 장치를 미미(mimi)라고 합시다.

1.  미미에 탑재한 센서에서 읽은 값을 broadcasting 만 목적으로 한다. 

 => BLE advertizement

2. 미미에 탑재한 센서에서 읽은 값을 연결되었을때만 데이타를 읽어갈 수 있도록 한다.

 => BLE service characteristics

3. 미미에 탑제된 여러 장치들을 제어하려고 한다.

 => connect /read /write

보통 목적은 3가지 형태가 될것 같습니다.

 

1번의 경우는 BLE advertizement를 이용하는 방법을 사용합니다.

- connection 없이 모단말(ble에선 central이라 부름)에서 scanning을 할때 scanning 한 데이타를 활용합니다.

- 이를 위헤서 미미(ble장치)에서도 당연히 advertizement 에 데이타를 설정 해줘야합니다.

- spec이나 가이드 상으로는 advertizementdata에 service data 항목이 있어서 service id와 data를 설정할 수 있습니다.

- 그러나 이렇게 사용할 수 없다면, manufacturer data를 활용하는 방법으로 할 수 도 있습니다.

- 여러 단말에서 동시에 읽어갈 수 있습니다.

 

2번의 경우는 BLE service의 characteristic의 값을 읽는 방식을 취합니다.

- scan후에 scan된 기기에 connect하고 나서 data를 읽어가는 방식입니다.

- 1번의 경우에는 scan만으로 정보를 읽어갈 수 있지만, 보안상 취약합니다. 모든 기기가 값을 다 볼 수 있기 때문이죠.

- 이런 connection 방식은 1:1통신이기 때문에, 연결되고 나면 다른 기기에서는 scan이 안됩니다.

- 때문에 connect후에 data를 읽고 바로 연결을 끊어야 합니다.

 

3번의 경우는 2번과 유사한데 다만 scanning 과 분리해서 connect후 characteristic에 read/write를 하는 방식으로 처리합니다.

- serial 방식( classic 방식) 과 유사한 요청사항이죠.

- 하지만 serical에서는 연결후 data를 보내고 response를 받는 것만 고려 했다면, BLE에서는 특정 characteristic정하고 해당 characteristic 값에 data를 wrtie하고 notifiy 를 설정하고 read를 하는 방식이라, 단순한 기능 구현에는 번거로움이 있지만,

미미(mini, ble 장치)가 복잡하다면 각 기능 제어를 분리해서 사용할 수 있기 때문에 좋습니다.

 

 

device scanning 예제 입니다.

package 는 flutter_blue_plus 를 사용했습니다.

 

pubspec.yaml


  flutter_blue_plus: ^1.3.1

 

AndroidManifest.xml

project/android/app/src/main/AndroidManifest.xml

project/android/app/src/profile/AndroidManifest.xml

 

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

 

Android platform 에서 GPS가 켜져있지 않으면, Bt device scan이 안됩니다.

그냥 BT 기기를 사용하는 입장에서는 GPS 없이도 device 인식이 가능한 것이 상식적일 것 같은데, 실제로 android에서는 그렇지 못합니다.

https://stackoverflow.com/questions/33045581/location-needs-to-be-enabled-for-bluetooth-low-energy-scanning-on-android-6-0

 

Location needs to be enabled for Bluetooth Low Energy Scanning on Android 6.0

After upgrading to Android version 6.0 Bluetooth Low Energy (BLE) scanning will only work if Location services are enabled on the device. See here for reference: Bluetooth Low Energy startScan on A...

stackoverflow.com

이럴경우  android:usesPermissionFlags="neverForLocation" 를 이용해서 scan이 가능하도록 설정 할 수 있습니다.

이 Flag는 상당히 미묘한데요.

위에 stackover flow 를 읽어보면, 위치정보를 기반으로 becon ble 등을 인식 하는 서비를 위해 GPS가 있을떄만 scan 이 되도록 한것 같은데, (그 외에 사생활 보안 등등의 이슈?? 도 있겠죠?)

서비스를 운영하는 입장에서 누군가가 GPS가 꺼져 있으니 동작 안합니다.(정확히는 BT가 scan 안됩니다). 라고 한다면, 답하기가 좀 곤란합니다. 

아무튼 플랫폼 정책상 GSP 꺼져있을때 scan이 안되도록 한 이유는 있겠지만, 우리는 항상 잘 BT 기기를 찾아야 하니 neverForLocation 같은 옵션을 사용하게 됩니다.

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" 
                     android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

 

 

 

Sample code

import 'LabBleGenericController.dart';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';



class LabBleDeviceGenericPage extends StatefulWidget{
  const LabBleDeviceGenericPage({super.key});

  @override
  State<LabBleDeviceGenericPage> createState() => _LabBleDeviceGenericPageState();
}


class _LabBleDeviceGenericPageState extends State<LabBleDeviceGenericPage> {
  StringBuffer data =StringBuffer();

  LabBleGenericController controller= LabBleGenericController();

  bool connectAll = true;
  bool isScanning = false;

  startScan()async{
    print("start scan!!!!!!!");
    isScanning = true;
    controller.scan(
        onScan:(val)=>setState(() { }),
        onScanDone:(){
          setState(() { });
          if(isScanning) {
            startScan();
          }
        });
  }
  stopScan() async{
    isScanning = false;
    await controller.stop();
    setState(() {
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title:Text("BleDevice")),
      body:Container(
        child: Column(children:[
          TextField(
            controller: controller.filterEdit,
            decoration: InputDecoration(prefix: Text("Filter:"),border: OutlineInputBorder()),
          ),
          SizedBox(height: 20,),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              ElevatedButton(
                  onPressed: (){
                    setState(() { data.clear(); });
                    if(isScanning){
                      stopScan();
                    }else{
                      startScan();
                    }
                  },
                  child: Text(isScanning?"Stop":"Scan")),
              ElevatedButton(
                  onPressed: (){
                    connectAll?controller.connectAll(): controller.disconnectAll();
                    connectAll =! connectAll;
                  },
                  child: Text(connectAll?"connect all":"disconnect all")),
              ElevatedButton(
                  onPressed: (){
                      setState(() { controller.updateDevices();});
                  },
                  child: Text("update")),
            ],
          ),

          SizedBox(height: 50,),
          

          Expanded(
            child: SingleChildScrollView(
              child: Column(
                children: [
                  SizedBox(
                      child: Text(controller.devicesStatus)),
                  SizedBox(
                      child: Text(controller.getResult()))
                ],
              ),
            ),
          ),

         

        ]),
      ),
    );
  }

  @override
  void dispose() {
    controller.close();
    super.dispose();
  }
}

 

controller

import 'dart:async';

import 'package:flutter/cupertino.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart';



class LabBleGenericController extends GetxController{

  StreamSubscription? sub;

  StringBuffer resBuffer =StringBuffer();
  final filterEdit =TextEditingController();
  
  final devices = <BluetoothDevice>[];

  LabBleGenericController(){
    filterEdit.text = "arbot";
  }


  @override
  void onClose() {
    print("LabBleGenericController:onClose()");
    sub?.cancel();
    super.onClose();
  }

  Future connectAll() async{
    for(final d in devices){
      d.connect();
    }
    updateDevices();
  }


  closeAll() async{


    for(final d in await FlutterBluePlus.instance.connectedDevices){
      await d.disconnect();
    }
    devices.clear();
  }

  disconnectAll() async{

    for(final d in devices){
      await d.disconnect();
    }
    updateDevices();
  }

  stop()async{
    await sub?.cancel();
    await FlutterBluePlus.instance.stopScan();
    sub= null;
  }

  scan({Function(ScanResult)? onScan,Function()? onScanDone}) async{

    await updateDevices();
    sub?.cancel();
    resBuffer.clear();

    await FlutterBluePlus.instance.stopScan();
    await closeAll();

    sub = FlutterBluePlus.instance.scan(timeout: Duration(seconds: 10)).listen((ScanResult result) {
      final localName = result.advertisementData.localName;
      if(!localName.toLowerCase().contains(filterEdit.text)) return;
      
      devices.add(result.device);
      
      print("localname:$localName");
      resBuffer.writeln(localName);
      resBuffer.writeln("[advertisementData]");
      resBuffer.writeln("manufacturerData:");

      int i =0;
      for(final k in result.advertisementData.manufacturerData.keys){
        final data = result.advertisementData.manufacturerData[k];
        resBuffer.write("\nkey($k):");
        if(data == null) continue;
        print("idx[$i]:${data.toString()}");
        print("str: ${String.fromCharCodes(data)}");
        resBuffer.writeln(" ${data.toString()}");
        i++;
      }
      resBuffer.writeln("[serviceUuids:${result.advertisementData.serviceUuids.length}]");
      i=0;
      for(final d in result.advertisementData.serviceUuids){
        resBuffer.writeln("[$i]: $d");
        i++;
      }
      resBuffer.writeln("serviceData:");
      i=0;
      for(final k in result.advertisementData.serviceData.keys){
        resBuffer.write("\n$k:");
        final data = result.advertisementData.serviceData[k];
        if(data == null) continue;
        print("idx[$i]:${data.toString()}");
        print("str: ${String.fromCharCodes(data)}");
        resBuffer.write("$data");
        i++;
      }
      print("buffer[${resBuffer.length}]:${resBuffer.toString()}");
      resBuffer.writeln("----------");
      onScan?.call(result);
    },
    onDone: (){
      sub?.cancel();
      onScanDone?.call();
      return ;
    });
  }

  String getResult(){
    return resBuffer.toString();
  }

  final _devicesStatus = "".obs;
  get devicesStatus => _devicesStatus.value;

  updateDevices() async{
    final buff = StringBuffer();
    for(final d in devices){
      buff.writeln("[${d.name}]: ${(await d.state.first).toString()}");
    }
    _devicesStatus.value =  buff.toString();
  }

  close(){
    sub?.cancel();
  }
}

 

해피 코딩!!