flutter ble 연동하기

나고수·2023년 12월 29일
0

Flutter

목록 보기
4/8
post-thumbnail

flutter와 ble 연동해서 데이터 주고받기

사용한 라이브러리는 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();
  }
}
profile
되고싶다

0개의 댓글