요즘 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에서는 그렇지 못합니다.
이럴경우 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();
}
}
해피 코딩!!
'Dart,Flutter' 카테고리의 다른 글
[Flutter] Googlemap 사용하기, pinch zoom, gesture 처리 (0) | 2023.05.09 |
---|---|
[Flutter] Camera 사용하기 (0) | 2023.04.28 |
[Flutter] web 실행시 httpRequest error 발생 (0) | 2023.02.02 |
[Flutter] flutter 3.7 google map 버그 (frame이 남아있는 문제) (0) | 2023.01.29 |
[Flutter] enum을 이용한 ui resouce 관리(3. TextStyle) (0) | 2023.01.26 |