블루투스 기능 구현(2)
flutter_blue_plus를 이용해서 블루투스 기능을 구현해보겠습니다.
구현할 기능은 다음과 같습니다.
- 장비탐색
- 장비연결
- 장비데이터 송수신
세팅
flutter_blue_plus | Flutter Package
Flutter plugin for connecting and communicationg with Bluetooth Low Energy devices, on Android, iOS, and MacOS.
pub.dev
블루투스 기능을 사용하려면 세팅이 필요합니다. 공식 사이트에서 제공하는 방법으로 세팅했는데 주변에 있는 블루투스 장비를 인식을 못합니다;;;
그래서 공식 사이트의 깃허브에서 예제 코드가 있는데 여기서 세팅된 값을 가져왔습니다.
GitHub - boskokg/flutter_blue_plus: Flutter plugin for connecting and communicationg with Bluetooth Low Energy devices, on Andro
Flutter plugin for connecting and communicationg with Bluetooth Low Energy devices, on Android and iOS - GitHub - boskokg/flutter_blue_plus: Flutter plugin for connecting and communicationg with Bl...
github.com
먼저, 안드로이드 권한 설정을 해야합니다.
android/app/src/main/AndroidManifest.xml 파일에서 다음 코드를 입력합니다.
<!-- Allow Bluetooth -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- legacy for Android 11 or lower -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>
<!-- legacy for Android 9 or lower -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
중요!!
세팅 후 기존에 있는 어플을 삭제해야 정상적으로 권한이 설정됩니다.
장비 탐색
먼저, 블루투스 장비를 읽어서 화면에 표시하도록 하겠습니다.
<device_screen.dart>
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../widgets/scan_result_tilte.dart';
class DeviceScreen extends StatefulWidget {
const DeviceScreen({super.key});
@override
State<DeviceScreen> createState() => _DeviceScreenState();
}
class _DeviceScreenState extends State<DeviceScreen> {
bool _isScanning = false;
List<ScanResult> _scanResults = [];
late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
late StreamSubscription<bool> _isScanningSubscription;
@override
void initState() {
super.initState();
_scanResultsSubscription = FlutterBluePlus.scanResults.listen((results) {
_scanResults = results;
setState(() {});
});
_isScanningSubscription = FlutterBluePlus.isScanning.listen((state) {
_isScanning = state;
setState(() {});
});
}
@override
void dispose() {
_scanResultsSubscription.cancel();
_isScanningSubscription.cancel();
super.dispose();
}
Future onScanPressed() async {
try {
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
} catch (e) {
print("Start Scan Error: $e");
}
}
Future onStopPressed() async {
try {
FlutterBluePlus.stopScan();
} catch (e) {
print("Stop Scan Error: $e");
}
}
Future onRefresh() {
if (_isScanning == false) {
FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
}
setState(() {});
return Future.delayed(const Duration(milliseconds: 500));
}
Widget buildScanButton(BuildContext context) {
if (FlutterBluePlus.isScanningNow) {
return FloatingActionButton(
onPressed: onStopPressed,
backgroundColor: Colors.red,
child: const Icon(Icons.stop),
);
} else {
return FloatingActionButton(
onPressed: onScanPressed, child: const Text("SCAN"));
}
}
List<Widget> _buildScanResultTiles(BuildContext context) {
return _scanResults
.map(
(r) => ScanResultTile(
result: r,
//onTap: () => onConnectPressed(r.device),
),
)
.toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: onRefresh,
child: ListView(
children: <Widget>[
..._buildScanResultTiles(context),
],
),
),
floatingActionButton: buildScanButton(
context), // This trailing comma makes auto-formatting nicer for build methods.
); // This trailing comma makes auto-formatting nicer for build method
}
}
<scan_result_tilte.dart>
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
class ScanResultTile extends StatefulWidget {
const ScanResultTile({
super.key,
required this.result,
this.onTap,
});
final ScanResult result;
final VoidCallback? onTap;
@override
State<ScanResultTile> createState() => _ScranResultTitleState();
}
class _ScranResultTitleState extends State<ScanResultTile> {
Widget _buildTitle(BuildContext context) {
if (widget.result.device.platformName.isNotEmpty) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.result.device.platformName,
overflow: TextOverflow.ellipsis,
),
Text(
widget.result.device.remoteId.toString(),
style: Theme.of(context).textTheme.bodySmall,
)
],
);
} else {
return Text(widget.result.device.remoteId.toString());
}
}
@override
Widget build(BuildContext context) {
return ExpansionTile(
title: _buildTitle(context),
);
}
}
정상적으로 탐색되는 모습입니다.
장비 연결 및 해제
장비 연결 및 해제를 구현해보겠습니다.
<device_screen.dart>
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import '../widgets/scan_result_tilte.dart';
class DeviceScreen extends StatefulWidget {
const DeviceScreen({super.key});
@override
State<DeviceScreen> createState() => _DeviceScreenState();
}
class _DeviceScreenState extends State<DeviceScreen> {
bool _isScanning = false;
List<ScanResult> _scanResults = [];
late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
late StreamSubscription<bool> _isScanningSubscription;
@override
void initState() {
super.initState();
_scanResultsSubscription = FlutterBluePlus.scanResults.listen((results) {
_scanResults = results; // 스캔 결과 저장
setState(() {});
});
_isScanningSubscription = FlutterBluePlus.isScanning.listen((state) {
_isScanning = state; // 스캔 상태 저장
setState(() {});
});
}
@override
void dispose() {
_scanResultsSubscription.cancel();
_isScanningSubscription.cancel();
super.dispose();
}
// 스캔 시작
Future onScanPressed() async {
try {
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
} catch (e) {
debugPrint("Start Scan Error: $e");
}
}
// 스캔 중지
Future onStopPressed() async {
try {
FlutterBluePlus.stopScan();
} catch (e) {
debugPrint("Stop Scan Error: $e");
}
}
// 스캔 결과 새로고침
Future onRefresh() {
if (_isScanning == false) {
FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
}
setState(() {});
return Future.delayed(const Duration(milliseconds: 500));
}
// 디바이스 연결
Future onConnectPressed(BluetoothDevice device) async {
await device.connect().catchError((e) {
debugPrint("Connect Error: $e");
});
}
// 디바이스 연결 해제
Future onDisConnectPressed(BluetoothDevice device) async {
await device.disconnect().catchError((e) {
debugPrint("Connect Error: $e");
});
}
// 스캔 버튼
Widget buildScanButton(BuildContext context) {
if (FlutterBluePlus.isScanningNow) {
return FloatingActionButton(
elevation: 0,
onPressed: onStopPressed,
backgroundColor: Colors.red,
child: const Icon(Icons.stop),
);
} else {
return FloatingActionButton(
elevation: 0,
onPressed: onScanPressed,
backgroundColor: Colors.black,
child: const Text("SCAN"),
);
}
}
// 스캔 결과 리스트
List<Widget> _buildScanResultTiles(BuildContext context) {
return _scanResults
.map(
(r) => ScanResultTile(
result: r,
onConnect: () => onConnectPressed(r.device),
onDisConnect: () => onDisConnectPressed(r.device),
),
)
.toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: RefreshIndicator(
onRefresh: onRefresh,
child: ListView(
children: <Widget>[
..._buildScanResultTiles(context),
],
),
),
floatingActionButton: buildScanButton(context),
);
}
}
<scan_result_tilte.dart>
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
class ScanResultTile extends StatefulWidget {
const ScanResultTile({
super.key,
required this.result,
this.onConnect,
this.onDisConnect,
});
final ScanResult result;
final VoidCallback? onConnect;
final VoidCallback? onDisConnect;
@override
State<ScanResultTile> createState() => _ScranResultTitleState();
}
class _ScranResultTitleState extends State<ScanResultTile> {
BluetoothConnectionState _connectionState =
BluetoothConnectionState.disconnected;
late StreamSubscription<BluetoothConnectionState>
_connectionStateSubscription;
@override
void initState() {
super.initState();
_connectionStateSubscription =
widget.result.device.connectionState.listen((state) {
_connectionState = state;
setState(() {});
});
}
@override
void dispose() {
_connectionStateSubscription.cancel();
super.dispose();
}
bool get isConnected {
return _connectionState == BluetoothConnectionState.connected;
}
Widget _buildTitle(BuildContext context) {
// 디바이스 이름이 있으면 표시
if (widget.result.device.platformName.isNotEmpty) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
widget.result.device.platformName, // 디바이스 이름
overflow: TextOverflow.ellipsis,
),
Text(
widget.result.device.remoteId.toString(), // 디바이스 주소
style: Theme.of(context).textTheme.bodySmall,
)
],
);
}
// 디바이스 이름이 없으면 주소 표시
else {
return Text(widget.result.device.remoteId.toString());
}
}
Widget _buildConnectButton(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
onPressed: (widget.result.advertisementData.connectable)
? (isConnected)
? widget.onDisConnect // 연결되어 있으면 연결 해제
: widget.onConnect // 연결되어 있지 않으면 연결
: null, // 연결 불가능한 디바이스는 버튼 비활성화
child: isConnected ? const Text('DISCONNECT') : const Text('CONNECT'),
);
}
@override
Widget build(BuildContext context) {
return ExpansionTile(
title: _buildTitle(context),
trailing: _buildConnectButton(context),
);
}
}
다음계획
다음에는 데이터 송수신을 구현하겠습니다.
'OpenCowork > 자전거 도난 방지' 카테고리의 다른 글
[자전거 도난 방지] 06. 블루투스 데이터 송수신 (0) | 2023.10.24 |
---|---|
[자전거 도난 방지] 05. 블루투스 이해하기 (0) | 2023.10.18 |
[자전거 도난 방지] 04. 아두이노 블루투스 모듈 사용하기 (0) | 2023.10.15 |
[자전거 도난 방지] 02. 블루투스 기능 구현(1) (0) | 2023.10.12 |
[자전거 도난 방지] 01. 디자인 작업 (0) | 2023.10.10 |