사용한 라이브러리는 flutter_blue_plus 입니다.
예제코드가 너무 잘되어있어서 거의 예제 코드를 사용했지만,
구현 중 어려움을 겪은 부분 위주로 적었습니다.
ble GATT 구조 알아보기
ble uuid 설명
ble 데이터 받기 notify vs indicate 설명
mixin BleScanMixin<T extends StatefulWidget> on State<T> {
late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
late StreamSubscription<bool> _isScanningSubscription;
List<ScanResult> scanResults = [];
bool isScanning = false;
bool isFailBleSearch = false;
List<BluetoothDevice> _systemDevices = [];
int? rssi;
BluetoothConnectionState connectionState =
BluetoothConnectionState.disconnected;
late StreamSubscription<BluetoothConnectionState> connectionStateSubscription;
late StreamSubscription<bool> _isConnectingSubscription;
late StreamSubscription<bool> _isDisconnectingSubscription;
late StreamSubscription<int> _mtuSubscription;
//페이지 init 하면 바로 주변 ble 스캔 시작
void initState() {
_scanResultsSubscription = FlutterBluePlus.scanResults.listen((results) {
scanResults = results;
setState(() {});
}, onError: (e) {
debugPrint('##Scan Error: $e');
});
_isScanningSubscription = FlutterBluePlus.isScanning.listen((state) {
isScanning = state;
setState(() {});
});
//스캔 시작
startScan();
//스캔 실패
Future.delayed(const Duration(seconds: bleScanTimeOut), () {
if (scanResults.isEmpty) {
setState(() {
isFailBleSearch = true;
});
}
});
super.initState();
}
Future startScan() async {
try {
// 현재 시스템에 연결된 디바이스 목록을 검색합니다.
//- 이 목록에는 *모든* 앱에 연결된 디바이스가 포함됩니다.
//- *내 앱*에 연결하려면 여전히 device.connect()를 호출해야 합니다.
_systemDevices = await FlutterBluePlus.systemDevices;
} catch (e) {
debugPrint('##Scan Error: $e');
}
try {
// 안드로이드는 모든 advertisments를 요청할 때 느립니다,
// 대신 1/8만 요청합니다.
int divisor = Platform.isAndroid ? 8 : 1;
await FlutterBluePlus.startScan(
timeout: const Duration(seconds: bleScanTimeOut),
continuousUpdates: true,
continuousDivisor: divisor);
} catch (e) {
debugPrint('##Scan Error: $e');
}
setState(() {}); // force refresh of systemDevices
}
bool get isConnected {
return connectionState == BluetoothConnectionState.connected;
}
//재검색
Future onRefresh() {
scanResults.clear();
if (isScanning == false) {
FlutterBluePlus.startScan(
timeout: const Duration(seconds: bleScanTimeOut));
}
setState(() {});
return Future.delayed(const Duration(milliseconds: 500));
}
stopScan(){
FlutterBluePlus.stopScan();
try {
_scanResultsSubscription.cancel();
_isScanningSubscription.cancel();
connectionStateSubscription.cancel();
_mtuSubscription.cancel();
_isConnectingSubscription.cancel();
_isDisconnectingSubscription.cancel();
} catch (e) {}
}
void dispose() {
try {
_scanResultsSubscription.cancel();
_isScanningSubscription.cancel();
connectionStateSubscription.cancel();
_mtuSubscription.cancel();
_isConnectingSubscription.cancel();
_isDisconnectingSubscription.cancel();
} catch (e) {}
super.dispose();
}
}
BluetoothService? targetService;
mixin BleMixin<T extends StatefulWidget> on State<T> {
String dataFromBle = '';
List<BluetoothService> _services = [];
BluetoothCharacteristic get targetCharacteristics =>
targetService!.characteristics.last;
BluetoothCharacteristic get targetCharacteristicsRead =>
targetService!.characteristics
.where((e) => e.descriptors.isNotEmpty)
.first;
//mtu == 최대 전송 단위
//안드로이드에 필요
//이걸 하지 않으면 안드로이드는 데이터가 짤려서 들어옴
Future requestMtu(BluetoothDevice device) async {
try {
await device.requestMtu(223);
debugPrint('##Request Mtu: Success');
} catch (e) {
debugPrint('##Change Mtu Error: $e');
}
}
Future discoverServices(BluetoothDevice device) async {
try {
//서비스를 찾음
_services = await device.discoverServices();
debugPrint('##Discover Services: Success');
//타켓 서비스를 찾음
targetService =
_services.where((e) => e.uuid.toString() == serviceUUID).last;
debugPrint('##Target Service : ${targetService.toString()}');
//안드로이드의 경우 mtu 설정
await requestMtu(device);
//ble와 한번 연결 되면 remoteId를 저장함
//다음 번 앱을 켰을 때 주변 기기를 스캔 후 이 remoteId를 찾아 자동으로 연결함
await saveBleRemoteId(device.remoteId.toString());
if (mounted) {
setState(() {});
}
} catch (e) {
debugPrint('##Discover Services Error: $e');
}
}
saveBleRemoteId(String name) async {
await SharedPreference.setString(bleRemoteId, name);
}
//데이터를 구독함
Future subscribeDataWithCallback(
StringCallback onResultListener,
) async {
try {
await targetCharacteristicsRead.setNotifyValue(true);
targetCharacteristicsRead.onValueReceived.listen((value) {
//데이터를 받아서 utf8로 디코딩 후 콜백으로 전달함
onResultListener(utf8.decode(value));
});
} catch (e) {
debugPrint('##Subscribe : Fail ${e.toString()}');
}
}
//데이터 쓰기
Future writeData(String data) async {
try {
//utf8로 인코딩 해서 바이트로 변환
List<int> byte = utf8.encode(data);
await targetCharacteristics.write(byte,
withoutResponse:
targetCharacteristics.properties.writeWithoutResponse);
debugPrint('##Write Ble Data : $data');
debugPrint('##Write : Success');
if (targetCharacteristics.properties.read) {
await targetCharacteristics.read();
}
} catch (e) {
debugPrint('##Write : Fail ${e.toString()}');
}
}
}
typedef StringCallback = void Function(String result);
class BleConnectPage extends ConsumerStatefulWidget {
const BleConnectPage({super.key});
ConsumerState<BleConnectPage> createState() => _BleConnectPageState();
}
class _BleConnectPageState extends ConsumerState<BleConnectPage>
with BleMixin, BleScanMixin {
//ble 목록 중 아이템을 클릭하면 연결 시도
//기기와 연결
Future<void> connectBle(BluetoothDevice device) async {
device.connectAndUpdateStream().catchError((e) {
//연결 실패
});
connectionStateSubscription = device.connectionState.listen((state) async {
connectionState = state;
//연결됨
if (state == BluetoothConnectionState.connected) {
debugPrint('##Discover Connected: Success');
await discoverServices(device);
}
if (state == BluetoothConnectionState.connected && rssi == null) {
rssi = await device.readRssi();
}
if (!mounted) return;
setState(() {});
});
}
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
...scanResults.map(
(result) {
//아이템를 보여줌
//ios는 device.advName
//android는 device.platformName
//을 사용했다.
},
),
],
),
);
}
}
var bleConnectType = ConnectType.defaultStatus;
class HomePage extends StatefulWidget {
const HomePage({
super.key,
this.isFromWifiConnect = false,
});
final bool isFromWifiConnect;
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with BleMixin, BleScanMixin {
ScanResult? scanResult;
bool get isBleConnected => bleConnectType == ConnectType.connected;
var savedBleRemoteId = SharedPreference.getString(bleRemoteId);
connectBle(BluetoothDevice device) async {
debugPrint('##연결-home page');
await device.connect();
await discoverServices(device);
setState(() {
bleConnectType = ConnectType.connected;
});
}
changeBleConnectedType(ConnectType type) {
setState(() {
bleConnectType = type;
});
}
connectBleToApp() async {
changeBleConnectedType(ConnectType.connecting);
//폰에 연결된 ble 목록
var systemDevices = await FlutterBluePlus.systemDevices;
if (systemDevices.isEmpty) {
changeBleConnectedType(ConnectType.defaultStatus);
return;
}
for (var e in systemDevices) {
//이전에 연결한 ble와 동일한 ble를 찾아서 연결
if (e.remoteId.toString() == savedBleRemoteId) {
connectBle(e);
return;
}
}
}
initBleConnect() async {
//이미 연결된 경우
if (targetService != null) {
changeBleConnectedType(ConnectType.connected);
return;
}
//한번도 ble와 연결 한 적 없는 경우
if (savedBleRemoteId == null) {
stopScan();
return;
}
//ble가 폰에 연결되어있지만 앱과 연결 되어 있지 않은 경우
await connectBleToApp();
//이전에 ble를 연결한 적이 있지만 현재 해당 ble가 폰과 연결이 안되있는 경우
//자동 연결 로직
if (scanResults.isNotEmpty && !isBleConnected) {
//이전에 연결한 ble와 동일한 ble를 찾아서 연결
scanResult = scanResults.firstWhereOrNull(
(e) => e.device.remoteId.toString() == savedBleRemoteId,
);
if (scanResult != null) {
changeBleConnectedType(ConnectType.connecting);
connectBle(scanResult!.device);
stopScan();
}
}
}
Widget build(BuildContext context) {
//앱을 끈 후 다시 앱을 키면 ble와 앱의 연동이 끊어져있었다.
//android bond나
//FlutterBluePlus.systemDevices; FlutterBluePlus.connectedDevices;
//를 해도 검색이 되지 않았다.
//그래서 그냥 홈 화면에서 항상 ble 기기를 스캔 후
//sharedPreference에 등록된 remoteId와 동일한 remoteId를 가진
//기기를 찾아서 연결 하도록 구현했다.
//앱을 키면 주변 기기를 스캔하고 타겟 디바이스를 찾아서 연결하는 과정이
//몇초 정도 걸리긴 했지만 , 최선의 방법이었다.
//앱을 처음 켰을 때 ble가 앱에 연결되어있는지 확인
initBleConnect();
debugPrint('##isBleConnected: $bleConnectType');
return const SizedBox();
}
}