ESP32와 Flutter간 Bluetooth 통신
2025.05.19 - [개발 이야기/개발 및 서비스] - ESP32 IDF + VSCode 환경 구축
이번 포스팅에서는 Flutter 앱에서 Bluetooth Low Energy (BLE) 장치를 스캔하고 연결하는 방법을 알아보겠습니다. IoT 장치와의 연동이나 웨어러블 기기 제어 등 다양한 분야에서 활용될 수 있는 중요한 기능이니, 차근차근 따라오시면 어렵지 않게 구현하실 수 있을 거예요.
1. flutter_blue
패키지 추가하기
Flutter에서 BLE 기능을 사용하려면 flutter_blue
패키지를 사용하는 것이 일반적입니다. 먼저 pubspec.yaml
파일에 다음과 같이 패키지를 추가해주세요.
dependencies:
flutter:
sdk: flutter
flutter_blue: ^0.8.0 # 최신 버전으로 업데이트될 수 있습니다.
패키지 버전은 현재 시점을 기준으로 0.8.0
을 사용했지만, 이 글을 보시는 시점에는 더 최신 버전이 있을 수 있으니 pub.dev에서](https://pub.dev/packages/flutter_blue)에서) 최신 버전을 확인하시는 것을 권장합니다.
패키지를 추가했다면 터미널에서 다음 명령어를 실행하여 의존성을 설치합니다.
flutter pub get
2. 플랫폼별 권한 설정
BLE 기능을 사용하기 위해서는 Android와 iOS 각 플랫폼에서 필요한 권한을 명시적으로 요청해야 합니다.
Android 권한 설정
android/app/src/main/AndroidManifest.xml
파일을 열고 <application>
태그 위에 다음 권한들을 추가해주세요.
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
...
</application>
참고: Android 6.0 (API 23)부터는 BLE 스캔을 위해 위치 권한이 필요합니다. 이는 BLE 스캔을 통해 사용자의 위치를 추정할 수 있기 때문입니다. 따라서 ACCESS_COARSE_LOCATION
또는 ACCESS_FINE_LOCATION
권한이 필수적입니다.
iOS 권한 설정
ios/Runner/Info.plist
파일을 열고 <dict>
태그 안에 다음 키-값 쌍을 추가해주세요.
<dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Need BLE permission</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Need BLE permission</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Need Location permission</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Need Location permission</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Need Location permission</string>
...
</dict>
iOS에서도 마찬가지로 BLE 사용을 위한 Bluetooth 권한과 위치 기반 서비스 (BLE 스캔)를 위한 위치 권한을 요청해야 합니다. NSBluetoothAlwaysUsageDescription
등 각 키에 대한 string
값은 사용자에게 권한 요청 시 보여지는 문구이므로, 앱의 목적에 맞게 명확하게 작성하는 것이 좋습니다.
3. BLE 스캔 및 연결 샘플 코드
이제 Flutter 앱에서 BLE 장치를 스캔하고 선택하여 연결하는 간단한 샘플 코드를 살펴보겠습니다.
import 'package:flutter/material.dart';
import 'package:flutter_blue/flutter_blue.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter BLE Scan Example', // 앱 제목 추가
theme: ThemeData(primarySwatch: Colors.blue), // 앱 테마 설정
home: BleHomePage(),
);
}
}
class BleHomePage extends StatefulWidget {
const BleHomePage({super.key});
@override
State<BleHomePage> createState() => _BleHomePageState();
}
class _BleHomePageState extends State<BleHomePage> {
FlutterBlue flutterBlue = FlutterBlue.instance;
List<BluetoothDevice> devicesList = []; // 스캔된 장치 목록
// 장치 스캔 시작
void scanDevices() {
devicesList.clear(); // 기존 목록 초기화
flutterBlue.startScan(timeout: const Duration(seconds: 4)); // 4초 동안 스캔
// 스캔 결과를 리스닝하여 장치 목록 업데이트
flutterBlue.scanResults.listen((results) {
for (ScanResult r in results) {
// 이미 목록에 없는 장치만 추가
if (!devicesList.contains(r.device)) {
setState(() {
devicesList.add(r.device);
});
}
}
});
// 스캔이 끝나면 stopScan을 호출하여 리소스 낭비 방지
flutterBlue.stopScan();
}
@override
void initState() {
super.initState();
// 앱 시작 시 Bluetooth 상태를 확인하고 사용자에게 활성화 요청
flutterBlue.state.listen((state) {
if (state == BluetoothState.off) {
// Bluetooth가 꺼져 있다면 사용자에게 켜달라고 요청하는 UI를 띄울 수 있습니다.
// 예를 들어, AlertDialog를 통해 사용자에게 안내합니다.
_showBluetoothOffDialog();
}
});
}
// Bluetooth가 꺼져 있을 때 보여줄 다이얼로그
void _showBluetoothOffDialog() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Bluetooth Off'),
content: const Text('Please turn on Bluetooth to use this app.'),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('BLE 장치 스캔')),
body: devicesList.isEmpty
? const Center(child: Text('장치를 스캔하려면 돋보기 아이콘을 눌러주세요.')) // 스캔 전 메시지
: ListView.builder(
itemCount: devicesList.length,
itemBuilder: (context, index) {
BluetoothDevice d = devicesList[index];
return ListTile(
title: Text(d.name.isEmpty ? '(이름 없음)' : d.name), // 장치 이름 표시
subtitle: Text(d.id.toString()), // 장치 ID (MAC 주소) 표시
onTap: () async {
try {
await d.connect(); // 장치 연결 시도
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${d.name.isEmpty ? '이름 없는 장치' : d.name}에 연결되었습니다!')),
);
// TODO: 연결 후 서비스 탐색 및 데이터 송수신 로직 구현
// 예: Navigator.push(context, MaterialPageRoute(builder: (context) => DeviceControlScreen(device: d)));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('연결 실패: $e')),
);
}
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: scanDevices, // 돋보기 아이콘 클릭 시 스캔 시작
child: const Icon(Icons.search),
),
);
}
}
코드 설명
MyApp
: 앱의 기본 구조를 설정하는 위젯입니다.MaterialApp
을 사용하여 앱의 제목과 테마를 설정했습니다.BleHomePage
: BLE 스캔 및 장치 목록을 표시하는 메인 페이지입니다._BleHomePageState
:flutterBlue
:flutter_blue
패키지의 인스턴스를 가져옵니다. 이 인스턴스를 통해 BLE 기능을 제어합니다.devicesList
: 스캔된 BLE 장치들을 저장할 리스트입니다.scanDevices()
: 이 함수가 호출되면 BLE 스캔을 시작합니다.flutterBlue.startScan(timeout: const Duration(seconds: 4))
: 4초 동안 주변 BLE 장치를 스캔합니다.timeout
을 설정하여 무한정 스캔하는 것을 방지할 수 있습니다.flutterBlue.scanResults.listen((results) {...})
: 스캔 결과가 나올 때마다results
리스트를 통해 장치 정보를 받아옵니다. 새로운 장치가 발견되면devicesList
에 추가하고 UI를 업데이트합니다.flutterBlue.stopScan()
: 스캔이 끝나면 자동으로 스캔을 중지하도록 설정했습니다. 이는 배터리 소모를 줄이는 데 중요합니다.
initState()
: 위젯이 처음 생성될 때 호출됩니다. 여기서는 Bluetooth 모듈의 현재 상태를 리스닝하여 Bluetooth가 꺼져 있을 경우 사용자에게 알림을 줍니다._showBluetoothOffDialog()
: Bluetooth가 꺼져 있을 때 사용자에게 Bluetooth를 켜도록 안내하는 다이얼로그를 보여줍니다.build()
:AppBar
: 앱 상단에 제목을 표시합니다.body
:devicesList
가 비어있으면 스캔 요청 메시지를, 장치가 있다면ListView.builder
를 사용하여 스캔된 장치 목록을 표시합니다.ListTile
: 각 장치의 이름과 ID를 보여주고, 장치를 탭하면d.connect()
를 호출하여 해당 장치에 연결을 시도합니다. 연결 성공/실패 여부를SnackBar
로 사용자에게 알립니다.FloatingActionButton
: 하단에 돋보기 아이콘 버튼을 추가하여 이 버튼을 누르면scanDevices()
함수가 호출되어 BLE 스캔을 시작합니다.
4. 실행 및 테스트
이제 앱을 실행하고 하단의 돋보기 아이콘을 눌러보세요. 주변의 BLE 장치들이 리스트에 나타나는 것을 확인할 수 있습니다. 리스트에 표시된 장치 중 연결을 원하는 디바이스를 선택하면 해당 장치와 연결을 시도합니다.
연결에 성공했다면, 이제 해당 장치의 서비스(Service)와 특성(Characteristic)을 탐색하여 데이터를 읽거나 쓰는 등 본격적인 BLE 통신을 구현할 수 있습니다. 이는 다음 단계에서 더 자세히 다루겠지만, 기본적인 스캔 및 연결 과정은 위 코드로 충분합니다.
참고:
- 실제 BLE 통신: 이 샘플 코드는 BLE 장치 스캔 및 연결까지만 다룹니다. 연결된 장치와 데이터를 주고받기 위해서는 장치의 서비스 UUID, 특성 UUID 등을 알아내고
flutter_blue
패키지의discoverServices()
,readCharacteristic()
,writeCharacteristic()
등의 메서드를 활용해야 합니다. - 에러 처리: 실제 앱 개발 시에는 Bluetooth가 꺼져 있거나, 권한이 부여되지 않았거나, 장치 연결에 실패하는 등 다양한 예외 상황에 대한 견고한 에러 처리가 필수적입니다.
- 백그라운드 스캔: 앱이 백그라운드에 있을 때도 스캔을 계속하려면 추가적인 설정과 권한이 필요하며, 플랫폼별 제약 사항을 고려해야 합니다.
이 포스팅이 Flutter 앱에서 BLE 기능을 시작하는 데 도움이 되었기를 바랍니다. 질문이 있으시면 언제든지 댓글로 남겨주세요!