[Android App BLE] #4 BLE 장치 Scan 구현

in #kr6 years ago (edited)

이전글 - [Android App BLE] #3 BLE를 위한 권한 설정 및 화면 구성

다음을 주로 참고하여 작성하였습니다.
Bluetooth Low Energy on Android, Part 1


출처: https://simbeez.com/


1. BLE 장치 스캔을 위한 변수 선언

BLE 장치를 스캔하는 코드를 작성해 보겠습니다.
먼저, BLE 관련 변수를 만듭니다.

public class MainActivity extends AppCompatActivity {
    // Tag name for Log message
    private final static String TAG="Central";
    // used to identify adding bluetooth names
    private final static int REQUEST_ENABLE_BT= 1;
    // used to request fine location permission
    private final static int REQUEST_FINE_LOCATION= 2;
    // scan period in milliseconds
    private final static int SCAN_PERIOD= 5000;
    // ble adapter
    private BluetoothAdapter ble_adapter_;
    // flag for scanning
    private boolean is_scanning_= false;
    // flag for connection
    private boolean connected_= false;
    // scan results
    private Map<String, BluetoothDevice> scan_results_;
    // scan callback
    private ScanCallback scan_cb_;
    // ble scanner
    private BluetoothLeScanner ble_scanner_;
    // scan handler
    private Handler scan_handler_;

스캔만 구현하는데 이렇게 많은 변수들이 필요합니다. 그러나 겁먹지 마시고요. 천천히 따라서 하시면 됩니다.

2. Central 장치의 BLE 지원 체크

먼저 onResume 함수에 Central 장치가 BLE를 지원하는지 검사하는 내용을 추가합니다.

@Override
    protected void onResume() {
        super.onResume();

        // finish app if the BLE is not supported
        if( !getPackageManager().hasSystemFeature( PackageManager.FEATURE_BLUETOOTH_LE ) ) {
            finish();
        }
    }

BLE 기능이 지원되지 않으면 바로 앱을 종료하게끔 해주는게 좋다고 합니다.

3. BLE adapter 설정

다음으로 BLE adapter를 설정합니다. onCreate 함수에 다음과 같이 코딩합니다.

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
       (생략)
       // ble manager
        BluetoothManager ble_manager;
        ble_manager= (BluetoothManager)getSystemService( Context.BLUETOOTH_SERVICE );
        // set ble adapter
        ble_adapter_= ble_manager.getAdapter();
}
  • BLE manager 설정
  • BLE manager를 이용하여 BLE adapter 설정

이렇게 하면 스캔을 하기 위한 기본 준비는 완료가 됩니다.

4. BLE 스캔

스캔을 시작하기 전에 몇가지 확인이 필요합니다.

  • Central 장치에 블루수트 어댑터가 설정되었는가?
  • Central 장치에 블루투스가 Enable 되어 있는가?
  • 이미 스캔중인건 아닌가?
  • FINE_LOCATION 사용 허가는 받았는가?

4.1 스캔 조건 체크

위의 내용들을 점검하는 코드를 작성합니다.

/*
    Start BLE scan
     */
    private void startScan( View v ) {
        tv_status_.setText("Scanning...");
        // check ble adapter and ble enabled
        if (ble_adapter_ == null || !ble_adapter_.isEnabled()) {
            requestEnableBLE();
            tv_status_.setText("Scanning Failed: ble not enabled");
            return;
        }
        // check if location permission
        if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            requestLocationPermission();
            tv_status_.setText("Scanning Failed: no fine location permission");
            return;
        }
    }

    /*
    Request BLE enable
    */
    private void requestEnableBLE() {
        Intent ble_enable_intent= new Intent( BluetoothAdapter.ACTION_REQUEST_ENABLE );
        startActivityForResult( ble_enable_intent, REQUEST_ENABLE_BT );

    }

    /*
    Request Fine Location permission
     */
    private void requestLocationPermission() {
        requestPermissions( new String[]{ Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_FINE_LOCATION );
    }

startScan( View v) 함수는 버튼 클릭 이벤트를 처리해야 해서 View를 인자로 받습니다. requestEnalbeBLE함수와 requestLocationPermission함수는 위에서 언급한 조건들을 만족하도록 코딩합니다.

여기까지는 별 무리없습니다. 특별히 BLE와 관련된 내용도 없구요. 이제부터가 본격적으로 BLE와 관련된 내용들입니다.

BLE는 startScan이라는 함수가 제공됩니다. 그런데 이 함수를 사용하기 위해서는 세가지 인자가 필요합니다.

  • ScanFilter: 스캔할 장치를 필터링. 예) 특정 MAC 주소의 장치만 스캔
  • ScanSettings: 스캔 모드를 설정. 예) 저전력 모드로 스캔
  • ScanCallback: 스캔 결과를 처리하는 콜백함수

4.2 ScanFilter 설정

원래는 ScanFilter에 특정 장치의 서비스 UUID를 입력하여 해당 서비스를 제공하는 장치만 스캔하려고 했는데, 실패했습니다. 대부분의 참고 사이트에서 이와 같은 방식을 사용하는데, 제 경우는 실패했습니다. 일단 일반적인 방법을 살펴보겠습니다.

먼저, Peripheral 장치에서 제공하는 서비스 UUID와 장치의 MAC ADDRESS를 설정합니다. 여기서 나타난 UUID는 Foc.us V3라는 장치입니다.

public class MainActivity extends AppCompatActivity {
   (생략)
    public static String SERVICE_STRING = "0000aab0-f845-40fa-995d-658a43feea4c";
    public static UUID UUID_TDCS_SERVICE= UUID.fromString(SERVICE_STRING);
    public static String CHARACTERISTIC_COMMAND_STRING = "0000AAB1-F845-40FA-995D-658A43FEEA4C";
    public static UUID UUID_CTRL_COMMAND = UUID.fromString( CHARACTERISTIC_COMMAND_STRING );
    public static String CHARACTERISTIC_RESPONSE_STRING = "0000AAB2-F845-40FA-995D-658A43FEEA4C";
    public static UUID UUID_CTRL_RESPONSE = UUID.fromString( CHARACTERISTIC_RESPONSE_STRING );
    public final static String MAC_ADDR= "78:A5:04:58:A7:92";
   (생략)

그럼 이제 startScan 함수에 위와 같은 서비스를 제공하는 장치만 스캔하도록 ScanFilter를 설정합니다.

private void startScan( View v ) {
(생략)
  // setup scan filters
  List<ScanFilter> filters= new ArrayList<>();
  ScanFilter scan_filter= new ScanFilter.Builder()
     .setServiceUuid( new ParcelUuid( UUID_TDCS_SERVICE ) )
     .build();
  filters.add( scan_filter );
(생략)

위 코드에서 보시면 UUID_TDCS_SERVICE 부분에서 해당 UUID 서비스만 스캔하도록 합니다. 그런데 어째 제가 가진 Peripheral 장치에서는 이렇게 설정하면 아무 장치 검색이 안됩니다. 이 부분은 직접 해보시길 바랍니다. 아마도 되는게 정상일 것입니다.

그래서 저는 하는 수 없이 MAC Address를 필터링하는 방식을 택하기로 했습니다.
참고 https://github.com/Parksunggyun/MyBeaconScanner/blob/master/app/src/main/java/altong/mon/mybeaconscanner/MainActivity.java

서비스 UUID를 지정하는 방식이 아니라 Peripheral 장치의 MAC address를 지정하는 방식으로 변경합니다.

private void startScan( View v ) {
(생략)
  //// set scan filters
  // create scan filter list
  List<ScanFilter> filters = new ArrayList<>();
  // create a scan filter with device mac address
  ScanFilter scan_filter = new ScanFilter.Builder()
        .setDeviceAddress( MAC_ADDR )
        .build();
  // add the filter to the list
  filters.add( scan_filter );
(생략)

이렇게 하면 특정 장치만 스캔할 수 있습니다. UUID 방식이 잘 되면 그 방식으로 하면 되겠습니다.

4.3 ScanSettings 설정

ScanSettings 설정은 간단합니다. 다음과 같이 저전력 모드로 스캔하도록 설정합니다.

private void startScan( View v ) {
(생략)
        //// scan settings
        // set low power scan mode
        ScanSettings settings= new ScanSettings.Builder()
                .setScanMode( ScanSettings.SCAN_MODE_LOW_POWER )
                .build();
(생략)

4.4 ScanCallback 설정

먼저 아래와 같이 콜백을 지정합니다.

private void startScan( View v ) {
(생략)
        scan_results_= new HashMap<>();
        scan_cb_= new BLEScanCallback( scan_results_ );
(생략)

BLEScanCallback는 콜백 클래스입니다. 사용자가 직접 구성해야 합니다. 다음과 같이 스캔 콜백에 필요한 함수들을 구현해 주면 됩니다. 코딩은 MainActivity 내부의 클래스로 작성합니다.

public class MainActivity extends AppCompatActivity {
   (생략)
    /*
    BLE Scan Callback class
    */
    private class BLEScanCallback extends ScanCallback {
        private Map<String, BluetoothDevice> cb_scan_results_;

        /*
        Constructor
         */
        BLEScanCallback( Map<String, BluetoothDevice> _scan_results ) {
            cb_scan_results_= _scan_results;
        }

        @Override
        public void onScanResult( int _callback_type, ScanResult _result ) {
            Log.d( TAG, "onScanResult" );
            addScanResult( _result );
        }

        @Override
        public void onBatchScanResults( List<ScanResult> _results ) {
            for( ScanResult result: _results ) {
                addScanResult( result );
            }
        }

        @Override
        public void onScanFailed( int _error ) {
            Log.e( TAG, "BLE scan failed with code " +_error );
        }

        /*
        Add scan result
         */
        private void addScanResult( ScanResult _result ) {
            // get scanned device
            BluetoothDevice device= _result.getDevice();
            // get scanned device MAC address
            String device_address= device.getAddress();
            // add the device to the result list
            cb_scan_results_.put( device_address, device );
            // log
            Log.d( TAG, "scan results device: " + device );
            tv_status_.setText( "add scanned device: " + device_address );
        }
    }
(생략)
  • 장치가 스캔되어 onScanResult가 호출되면addScanResult`함수를 호출
  • addScanResult함수에서 장치의 MAC address와 장치를 list에 추가

4.5 startScan 호출

이제 BLE에서 제공하는 starScan 함수를 호출하기 위한 모든 준비가 완료되었습니다. 아래와 같이 코딩합니다.

private void startScan( View v ) {
(생략)
        //// now ready to scan
        // start scan
        ble_scanner_.startScan( filters, settings, scan_cb_ );
        // set scanning flag
        is_scanning_= true;
(생략)

5. START SCAN 버튼 이벤트

아직 START SCAN 버튼 이벤트 핸들러를 만들지 않았습니다. 다음과 같이 onCreate 함수에 이벤트 핸들러를 추가합니다.

 @Override
    protected void onCreate(Bundle savedInstanceState) {
(생략)
        //// set click event handler
        btn_scan_.setOnClickListener( (v) -> { startScan(v); });
(생략)

그런데 위와 같이 lambda expressions are not supported at language level '1.7 와 같은 에러가 발생합니다. Java 버전이 1.7에서는 허용되지 않는 문법입니다. 따라서 build.gradle (Module: app) 파일에 다음과 같이 compileOptions을 1.8로 하여 추가합니다.

android {
(생략)
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

파일 수정후에는 스튜디오 우측 상단에 Sync Now 버튼을 클릭하여 싱크를 수행합니다. 싱크가 완료되면 발새했던 에러가 사라지는 것을 확인합니다.

6. Scan 결과 확인

이제 코드를 빌드하고 앱을 실행시켜 봅니다. 한가지 아쉬운 점은 Android Virtual Device에는 블루투스 에뮬레이션 기능이 없습니다. 저도 이제 알았습니다. 따라서 AVD로는 테스트 할 수 가 없습니다. 본인의 안드로이드 스마트폰에 연결하여 확인하도록 합니다.

빌드가 문제없이 되고, 앱이 스마트폰에 설치되면 자동으로 앱이 실행됩니다. 그리고 START SCAN 버튼을 누르니 저는 FINE_LOCATION 권한 요청이 뜹니다.

그런 후 다시 START SCAN 버튼을 누르니 장치 검색이 되었다는 메시지가 Status에 나타납니다.

그런데 말입니다. 스튜디오의 Log 창을 보니, 아래와 같습니다.

즉, 장치를 찾고 나서 계속 스캔을 하면서 또 장치를 찾고, 또 찾고를 반복하고 있습니다. 네 그렇습니다. 장치를 찾으면 스캔을 중지해야 합니다.

다음에 이 부분을 구현해 보겠습니다. 간단할 줄 알았던 스캔 부분이 매우 길어진 느낌입니다. 코딩하면서 문제도 발생하여 해결하고, 이래 저래 시간이 드네요. 그래도 다행이 원하는 대로 장치가 검색되니 기분은 좋습니다!


오늘의 실습: 스캔 코드도 완성되었으니 가지고 있는 장치가 얼마나 멀리에서도 스캔되는지 확인해 보세요.

Sort:  

BLE chip vendor 에 관계없이 동작하는지요?

특정 벤더에 대해 따로 설정하는 부분은 없습니다.
제가 테스트 하는 장치도 비주류 장치인데, 벤더 상관없이 동작하는거 같습니다.
BLE 표준을 따르는 장치는 다 되지 않을까요?

안녕하세요, 글 잘 보고있습니다! 블루투스 2.0모듈을 사용하고 있는데 scan버튼을 눌러도 scanning...만 뜨고 기기를 찾지 못합니다. scan이 안되는건 어디 문제일까요? 답변 꼭 부탁드립니다!!!

물어보신 내용이 구체적이지 않아 대답 드리기 어렵네요. 최소한 오류가 무엇인지 알려주셔야 합니다. 그러나 더 문제는 이게 5년전 자료라 지금과 달라졌을거 같네요.
필요흐시면 소스코드 찾아보고 공유드릴 수 있을거 같습니다.