[캡스톤_front] BLE 적용하기_클론코딩 1

피용희·2024년 4월 1일
0

2024 캡스톤

목록 보기
6/19

Intro

이제 BLE 개념에 대해 빠삭하게 알았으니 코드를 이용해서 BLE를 적용하는 방법을 낱낱이 알아보겠다.

코드 공부는 이 분의 코드를 클로론코딩하면서 기능을 익혀보는 방향으로 잡았다.
너무 좋은 자료 감사합니다ㅠㅠ!!

이 단계에서의 목표는, 우선 잘 정리 되어 있는 블로그 글을 클론코딩으로 따라해 본 뒤에 코드를 분석하면서 왜 이런 기능이 들어갔는지를 익히고, 코드를 클래스 단위로 분리해서 작동시켜 보면서 화면을 어떻게 분리할지를 예상해 보는 것!

우선 다음과 같이 main.dart 클론코딩을 완료했다.

전체 코드

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final title = 'Flutter BLE Scan Demo';

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: title,
      home: MyHomePage(title: title),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<ScanResult> scanResultList = []; //스캔 결과를 담기 위함
  bool _isScanning = false;

  BluetoothAdapterState _adapterState =
      BluetoothAdapterState.unknown; 
  late StreamSubscription<BluetoothAdapterState> _adapterStateStateSubscription;

  //이 부분 코드 공부 필요
  @override
  void initState() {
    //블루투스 초기화
    super.initState();
    _adapterStateStateSubscription =
        FlutterBluePlus.adapterState.listen((state) {
      _adapterState = state;
      if (mounted) {
        setState(() {});
      }
    });
  }

  //여기도 mount가 뭔지, 리스너가 정확히 뭔지 필요
  void initBle() {
    //블루투스 스캔 상태 얻기 위한 리스너
    FlutterBluePlus.isScanning.listen((isScanning) {
      _isScanning = isScanning;
      if (mounted) {
        setState(() {});
      }
    });
  }

  /*
  스캔 시작/정지 함수
   */
  //async = 느려지느 거라 붙이는 걸로 알고 있음
  scan() async {
    if (!_isScanning) {
      //스캔중이 아니라면,
      //기존에 스캔된 리스트 삭제
      scanResultList.clear();
      //스캔 시작, 제한 시간 5초
      FlutterBluePlus.startScan(timeout: Duration(seconds: 5));
      //스캔 결과 리스너
      //함수를 보니까. 리스너에는 (events) 이런식으로 들어가는 것 같다.
      FlutterBluePlus.scanResults.listen((results) {
        //List<ScanResult> 형태의 results 값을 scanResultList에 복사
        scanResultList = results;
        //ui 갱신
        setState(() {});
      });
    } else {
      //스캔 중이라면, 스캔 정지
      //궁금한게 굳이 이걸 왜 넣는걸까?
      FlutterBluePlus.stopScan();
    }
  }

  /*
  장치별 출력용 함수들
  나는 일단 device.id만 출력해본다.
   */
  Widget deviceMacAddress(ScanResult r) {
    return Text(r.device.remoteId.toString());
  }

  /* 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.advName}');
  }

  /* 장치 아이템 위젯 */
  // 이 부분도 어떤 방식인지는 공부가 필요할듯..
  Widget listItem(ScanResult r) {
    return ListTile(
      onTap: () => onTap(r),
      leading: leading(r),
      title: deviceMacAddress(r),//정의한 macAddress를 return
    );
  }

  /* UI */
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          /*장치 리스트 출력 */
          child: ListView.separated(
              itemBuilder: (context, index) {
                return listItem(scanResultList[index]);
              },
              separatorBuilder: (BuildContext context, int index) {
                return Divider(); //뭐...인덱스 별로 나눈다 이런건지...?
              },
              itemCount: scanResultList.length)),
      floatingActionButton: FloatingActionButton(
        onPressed: scan,

        //스캔 중이라면 stop 아이콘을, 정지 상태라면 search 아이콘을 표시
        child: Icon(_isScanning ? Icons.stop : Icons.search),
      ),
    );
  }
}

https://blog.naver.com/chandong83/222414483554

완성 화면

이제 코드를 하나하나 뜯어가면서 여기서 왜 이런 기능이 쓰였는지를 상세히 알아보자.
프론트 화면 ui를 위한 위젯 부분은 넘어가고, 블루투스 기능을 위주로 살펴보겠다

코드 뜯어보기

class _MyHomePageState extends State<MyHomePage> {
  List<ScanResult> scanResultList = []; //스캔 결과를 담기 위함
  bool _isScanning = false;

  BluetoothAdapterState _adapterState =
      BluetoothAdapterState.unknown;
  late StreamSubscription<BluetoothAdapterState> _adapterStateStateSubscription;

  @override
  void initState() {
    //블루투스 초기화
    super.initState();
    _adapterStateStateSubscription =
        FlutterBluePlus.adapterState.listen((state) {
      _adapterState = state;
      if (mounted) {
        setState(() {});
      }
    });
  }
  
  void initBle() {
    //블루투스 스캔 상태 얻기 위한 리스너
    FlutterBluePlus.isScanning.listen((isScanning) {
      _isScanning = isScanning;
      if (mounted) {
        setState(() {});
      }
    });
  }

  /*
  스캔 시작/정지 함수
   */
  //async = 느려지느 거라 붙이는 걸로 알고 있음
  scan() async {
    if (!_isScanning) {
      //스캔중이 아니라면,
      //기존에 스캔된 리스트 삭제
      scanResultList.clear();
      //스캔 시작, 제한 시간 5초
      FlutterBluePlus.startScan(timeout: Duration(seconds: 5));
      //스캔 결과 리스너
      //함수를 보니까. 리스너에는 (events) 이런식으로 들어가는 것 같다.
      FlutterBluePlus.scanResults.listen((results) {
        //List<ScanResult> 형태의 results 값을 scanResultList에 복사
        scanResultList = results;
        //ui 갱신
        setState(() {});
      });
    } else {
      //스캔 중이라면, 스캔 정지
      //궁금한게 굳이 이걸 왜 넣는걸까?
      FlutterBluePlus.stopScan();
    }
  }

해당 코드는 StatefulWidget이 실행하는 화면 부분이다. 이 부분에서 블루투스 초기화 및 블루투스 스캔 상태를 얻기 위한 리스너 실행, 스캔 시작이 이뤄진다.

초기값 설정

BluetoothAdapterState _adapterState =
      BluetoothAdapterState.unknown; 
  late StreamSubscription<BluetoothAdapterState> _adapterStateStateSubscription;
  • BluetoothAdapterState adapterState : BluetoothAdapterState 타입의 adapterState 변수를 선언하고, 초기값을 BluetoothAdapterState.unknown으로 설정하기 위한 코드이다. 이 변수는 Bluetooth 어댑터의 현재 상태를 추적하는 데 사용된다.
  • StreamSubscription< BluetoothAdapterState> _adapterStateStateSubscription : 스트림 변수를 선언한 것인데, Adapt의 상태변경을 수신하기 위한 StreamSubscription이다. 이 변수는 차후 init에서 초기화 된다.
  • 이 부분은 Bluetooth adapter의 상태를 추적하고, 상태가 변경될 때마다 해당 변경 사항을 처리하기 위한 준비를 하기 위한 과정이다.

초기화

  @override
  void initState() {
    //블루투스 초기화
    super.initState();
    _adapterStateStateSubscription =
        FlutterBluePlus.adapterState.listen((state) {
      _adapterState = state;
      if (mounted) {
        setState(() {});
      }
    });
  }

이 부분은 초기화를 위한 부분이다.

  • 왜 초기화가 필요할까?
    초기화는 프로그램이나 애플리케이션이 정상적으로 동작하고 안정성을 유지하기 위해 필요한 중요한 단계이다. 상태관리의 일부로써 상태를 초기화하고 관리함으로써 프로그램이나 애플리케이션이 예상대로 동작할 수 있도록 하는 역할을 한다.

initState 메서드는 해당 위젯이 상태 트리에 추가될 때 한 번만 호출되므로, 한 번만 실행되어야 하는 초기화 작업을 여기서 수행할 수 있다.

여기서는 블루투스를 사용하기 위해, initState를 override해서 재정의 한 후 상태를 초기화 해주고 있다.

  • super.initState() : 상위 클래스틔 initState 메서드를 호출한다. 이렇게 함으로써 상위 클래스의 initState 메서드가 중요한 초기화 로직을 실행할 수 있도록 한다.

  • _ adapterStateStateSubscription : 블루투스 어댑터 상태 변경을 감지하기 위한 StreamSubscription 객체이다. 블루투스 어댑터의 상태를 수신하기 위해 이 객체가 사용된다.
    위에서 선언했던 스트림 객체를 정의하여 adapt 상태 변경을 체크한다.

    • StreamSubscription 객체 : Dart 프로그래밍 언어에서 비동기적인 스트림 이벤트를 처리하기 위한 구독자를 나타내는 클래스이다. 스트림은 비동기적으로 발생하는 이벤트 시퀀스를 나타내는데, 예를 들어 사용자 입력, 네트워크 요청의 응답, 파일 읽기 등이 해당된다.StreamSubscription 객체는 이러한 스트림에서 이벤트를 구독하고, 이벤트가 발생할 때마다 이를 처리하는데 사용된다.
    • 비동기 이벤트 시퀀스? : 애플리케이션에서 발생하는 이벤트들이 순차적으로 발생하는 것이 아니라 동시에 혹은 불규칙적인 순서로 발생하는 것을 의미한다. 예를 들자면 파일 I/O 작업, 네트워크 요청 응답 등이 있다. 여기서는 상태 변경의 경우 불규칙적으로 발생하므로, 변화가 발생할때마다 이벤트를 수신하는 스트림을 사용해서 Adapter의 상태를 체크한다.
  • FlutterBluePlus.adapterState.listen((state) { adapterState = state;} : FlutterBluePlus의 adapterState 스트림에서 상태 변경을 구독하고 있다. 상태가 변경되면 해당 상태를 adapterState 변수에 할당하고, 이후에 mounted 상태인지 확인한 후 setState를 호출하여 UI를 업데이트한다.

    • mounted 상태 : state 객체가 현재 위젯트리에 있는지 여부를 나타낸다. 즉, 해당 State 객체가 위젯트리에 추가되었는지 또는 제거 되었는지를 판단한다. mounted 상태를 확인하는 이유는 위젯이 여전히 화면에 표시되어 있는지를 확인하기 위함입니다. 만약 위젯이 이미 제거되었다면 setState를 호출하면 오류가 발생할 수 있기 때문이다.
    • Dart에서 listen()은 Stream 클래스의 메서드 중 하나로, 비동기적인 이벤트 스트림을 구독하는 데 사용된다. 이 메서드를 이용해서 스트림에서 발생하는 이벤트를 수신하고 처리하는 함수를 등록할 수 있다.
    • 📌 즉, 여기서는 FlutterBluePlus.adapterState를 이용해서 sadaptState를 받아오는 스트림을 생성하고, listen을 통해 상태를 관찰한다. 그리고 상태를 _ adapterState라는 변수에 저장해서 adapter 상태를 확인한다.
      • adapterState의 retuen은 Stream< BluetoothAdapterState>이기 때문에 listen 사용이 가능하다!

스캐너 상태 확인

void initBle() {
    //블루투스 스캔 상태 얻기 위한 리스너
    FlutterBluePlus.isScanning.listen((isScanning) {
      _isScanning = isScanning;
      if (mounted) {
        setState(() {});
      }
    });
  }

이 코드는 위 코드와 형식상은 동일하다.
adapt 뿐 아니라 스캔 상태를 확인하기 위한 코드이다.
우리의 경우 이를 이용해서 스캔 상태가 종료 되었을시 해당하는 화면을 띄울 수 있을 것으로 기대된다.

스캔 시작

  /*
  스캔 시작/정지 함수
   */
 
  scan() async {
    if (!_isScanning) {
      //스캔중이 아니라면,
      //기존에 스캔된 리스트 삭제
      scanResultList.clear();
      //스캔 시작, 제한 시간 5초
      FlutterBluePlus.startScan(timeout: Duration(seconds: 5));
      //스캔 결과 리스너
      //함수를 보니까. 리스너에는 (events) 이런식으로 들어가는 것 같다.
      FlutterBluePlus.scanResults.listen((results) {
        //List<ScanResult> 형태의 results 값을 scanResultList에 복사
        scanResultList = results;
        //ui 갱신
        setState(() {});
      });
    } else {
      //스캔 중이라면, 스캔 정지
      FlutterBluePlus.stopScan();
    }
  }
  • if (!_isScanning) : 스캔중이 아닐때만 스캔을 하는 이유는 중복 스캔을 방지하기 위함이다.
  • 스캔 중인 동안에 다시 스캔을 시작하게 되면, 여러번의 스캔 요청이 발생하여 시스템 리소스가 낭비되고 애플리케이션의 성능이 저하된다. 또한 중복된 스캔 결과가 발생하여 결과의 일관성을 유지하기 어려워지므로 이를 체크해야 한다.
  • FlutterBluePlus.scanResults.listen((results) : 위에서 리스너를 통해 상태변경을 구독하고 있다고 말한 바가 있다. 스캔 결과 리스트는 이벤트가 발생할 때마다 즉각적으로 반영되어야 하기 때문에 listen을 이용해서 결과를 scanResult에 반영해준다.

log 살펴보기

다음 log는 실행 되었을때 나타난 log이다. 이를 분석해보자.

D/[FBP-Android]( 4191): [FBP] onMethodCall: startScan
D/BluetoothAdapter( 4191): isLeEnabled(): ON
D/BluetoothLeScanner( 4191): onScannerRegistered() - status=0 scannerId=3 mScannerId=0
D/EGL_emulation( 4191): app_time_stats: avg=3788.37ms min=563.35ms max=7013.39ms count=2
D/[FBP-Android]( 4191): [FBP] onMethodCall: stopScan
  • D/FBP-Android: [FBP] onMethodCall: startScan : 앱이 블루투스 스캔을 시작한다.
  • D/BluetoothAdapter( 4191): isLeEnabled() : ON : BLE (LE)가 활성화 되었음을 나타낸다. 이것은 Bluetooth 스캔이 LE 장치를 대상으로 할 것임을 보여준다.
  • D/BluetoothLeScanner( 4191): onScannerRegistered() - status=0 scannerId=3 mScannerId=0 : bluetooth LE 스캐너가 등록 되었음을 보여준다. status=0은 성공적으로 등록되었음을 나타내고, scannerId=3 mScannerId=0은 각각 스캐너의 ID를 나타낸다.
  • D/EGL_emulation( 4191): app_time_stats: avg=3788.37ms min=563.35ms max=7013.39ms count=2 : 앱 실행 시간 통계를 보여준다. 실행시간의 최소 및 최대 값을 제공한다.
  • D/FBP-Android: [FBP] onMethodCall: stopScan : 앱이 Bluetooth 스캔을 중지하는 것을 나타낸다.

결과 확인

휴대폰에 연결해서 결과를 살펴보자


profile
코린이

0개의 댓글

관련 채용 정보