ํ์ด์ ๋์ค ๋์ ํฌ์คํ ์ ํฌํจ๋ ์ฝ๋๋ฅผ ์ผ๋ถ ์์ ํ์์ต๋๋ค.
https://blog.naver.com/chandong83/222414483554
BLE ํต์ ์ค์ผ์ค ๋ฐ ์๋๋ก์ด๋ ๋์์ ๊ดํ ํฌ์คํ ์ ๋ณด๊ณ ์์ผ ์ด๋ ์ ๋ ์ดํดํ๊ธฐ ํธํ๋ค.
๐ โ1. ํจํค์ง ์ค์น ๋ฐ ์ค์
flutter_blue ๋์ flutter_blue_plus ํจํค์ง๋ฅผ ์ค์นํ๋ค.
flutter_blue๋ ๋ ์ด์ ์ ๋ฐ์ดํธํ์ง ์๊ณ ์๋๋ก์ด๋ 12 ๊ถํ ์ด์๊ฐ ์์ผ๋ฏ๋ก, flutter_blue_plus๋ฅผ ์ค์นํ๋ค.
$ flutter pub add flutter_blue_plus
์ค์นํ๊ฒ ๋๋ฉด ์์์ dependecies๊ฐ ์ถ๊ฐ๋๋ค.
โ๏ธ 1. android/app/build.gradle : SDK ์ต์ ๋ฒ์ 19๋ก ์ค์
Android {
defaultConfig {
minSdkVersion: 19
โ๏ธ 2. android/app/src/main/AndroidManifest.xml : Bluetooth ๊ถํ ์ค์
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
๐ 2. Code
ํ์ด ์ ๋์ค๋ ๊ฐ์ ๊ฒฝ์ฐ๋ ๋ฒํผ์ ๋๋ฅด๊ณ 4์ด ๋์ ์ค์บํ๊ณ , ๊ฒฐ๊ณผ๊ฐ ๋ณด์ธ๋ค๋ฉด, ๋ด ์ฝ๋๋ ์ค์บ์ ๊ณ์ ์งํํ๋ฉด์, RSSI ๋ณํ๋์ ๋ชจ๋ํฐ๋งํ๋ ๊ฒ์ด๋ค.
์ฝ๋์์ ํฐ ์ฐจ์ด๋ ์์ง๋ง, startScan ํจ์์์ allowDuplicates ํ๋ผ๋ฏธํฐ๋ฅผ true ์ค์ ํ ๊ฒ์ด๋ค.
allowDuplicates๋ ๊ธฐ๋ณธ false๋ก, ํ ๋ฒ ๋ฐ๊ฒฌ๋ ๊ธฐ๊ธฐ๋ ๋ ์ด์ ์ ๋ฐ์ดํธ๋์ง ์๊ณ RSSI ๊ฐ์ด ๊ณ ์ ๋๋ค.
์ด๋ฅผ true๋ก ์ค์ ํ๊ฒ ๋๋ฉด, ์ด๋ฏธ ๋ฐ๊ฒฌ๋ ๊ธฐ๊ธฐ๋ผ ํ ์ง๋ผ๋ ์ ๋ฐ์ดํธ๋๋ฏ๋ก RSSI ๋ณํ๋์ ํ์ธํ ์ ์๋ค.
ScanMode๋ ์ค๋ด ์ธก์๋ฅผ ์ํด LowLatency๋ฅผ ์ฌ์ฉํ๋ค. ๋ชจ๋์ ๋์์ ํ์ธํ๊ณ ์ถ์ผ๋ฉด ๋ชฉ์ฐจ์์ ๋๋ฒ ์งธ ๊ธ์ ๋ณด๋ฉด ๋๋ค.
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
final title = 'Flutter BLE Scan Demo';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: title,
home: MyHomePage(title: title),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
FlutterBluePlus flutterBlue = FlutterBluePlus.instance;
List<ScanResult> scanResultList = [];
int scan_mode = 2; // scan mode
bool isScanning = false;
/* ์์ฑ์ */
@override
void initState() {
super.initState();
}
/* ์์, ์ ์ง */
void toggleState() {
isScanning = !isScanning;
if (isScanning) {
flutterBlue.startScan(
scanMode: ScanMode(scan_mode), allowDuplicates: true);
scan();
} else {
flutterBlue.stopScan();
}
setState(() {});
}
/*
Scan Mode
Ts = scan interval
Ds = duration of every scan window
| Ts [s] | Ds [s]
LowPower | 5.120 | 1.024
BALANCED | 4.096 | 1.024
LowLatency | 4.096 | 4.096
LowPower = ScanMode(0);
BALANCED = ScanMode(1);
LowLatency = ScanMode(2);
opportunistic = ScanMode(-1);
*/
/* Scan */
void scan() async {
if (isScanning) {
// Listen to scan results
flutterBlue.scanResults.listen((results) {
// do something with scan results
scanResultList = results;
// update state
setState(() {});
});
}
}
/* ์ฅ์น์ RSSI */
Widget deviceSignal(ScanResult r) {
return Text(r.rssi.toString());
}
/* ์ฅ์น์ MAC ์ฃผ์ ์์ ฏ */
Widget deviceMacAddress(ScanResult r) {
return Text(r.device.id.id);
}
/* ์ฅ์น์ ๋ช
์์ ฏ */
Widget deviceName(ScanResult r) {
String name;
if (r.device.name.isNotEmpty) {
// device.name์ ๊ฐ์ด ์๋ค๋ฉด
name = r.device.name;
} else if (r.advertisementData.localName.isNotEmpty) {
// advertisementData.localName์ ๊ฐ์ด ์๋ค๋ฉด
name = r.advertisementData.localName;
} else {
// ๋๋ค ์๋ค๋ฉด ์ด๋ฆ ์ ์ ์์...
name = 'N/A';
}
return Text(name);
}
/* BLE ์์ด์ฝ ์์ ฏ */
Widget leading(ScanResult r) {
return const CircleAvatar(
backgroundColor: Colors.cyan,
child: Icon(
Icons.bluetooth,
color: Colors.white,
),
);
}
/* ์ฅ์น ์์ดํ
์ ํญ ํ์๋ ํธ์ถ ๋๋ ํจ์ */
void onTap(ScanResult r) {
// ๋จ์ํ ์ด๋ฆ๋ง ์ถ๋ ฅ
print('${r.device.name}');
}
/* ์ฅ์น ์์ดํ
์์ ฏ */
Widget listItem(ScanResult r) {
return ListTile(
onTap: () => onTap(r),
leading: leading(r),
title: deviceName(r),
subtitle: deviceMacAddress(r),
trailing: deviceSignal(r),
);
}
/* UI */
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
/* ์ฅ์น ๋ฆฌ์คํธ ์ถ๋ ฅ */
child: ListView.separated(
itemCount: scanResultList.length,
itemBuilder: (context, index) {
return listItem(scanResultList[index]);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider();
},
),
),
/* ์ฅ์น ๊ฒ์ or ๊ฒ์ ์ค์ง */
floatingActionButton: FloatingActionButton(
onPressed: toggleState,
// ์ค์บ ์ค์ด๋ผ๋ฉด stop ์์ด์ฝ์, ์ ์ง์ํ๋ผ๋ฉด search ์์ด์ฝ์ผ๋ก ํ์
child: Icon(isScanning ? Icons.stop : Icons.search),
),
);
}
}
๐ฅ 3. Demo Video
https://www.youtube.com/shorts/vdjfelsk_Ug