[Android App BLE] #5 BLE 장치 Connect 구현

in #kr6 years ago (edited)

이전글 - [Android App BLE] #4 BLE 장치 Scan 구현


출처: https://simbeez.com/


반복되는 스캔 문제 해결

먼저 이전글에서 스캔을 반복적으로 하는 문제 부터 해결해 보겠습니다. 이전 코드를 보면 스캔 콜백으로 인해 계속해서 스캔이 수행되고 콜백이 호출되게 되어 있습니다. 이 문제를 해결하기 위해 일정 시간 후에 스캔을 중지시키는 핸들러는 추가합니다. 그전에 스캔하는 시간을 정하는 변수를 정의합니다.

public class MainActivity extends AppCompatActivity {
{
   (생략)
    // scan period in milliseconds
    private final static int SCAN_PERIOD= 5000;
   (생략)

이제 startScan함수에 다음과 같이 핸들러를 추가합니다.

private void startScan( View v ) {
  (생략)
  scan_handler_= new Handler();
  scan_handler_.postDelayed( this::stopScan, SCAN_PERIOD );
}

scan_handler_는 우리가 설정한 스캔 시간이 지나면 지정된 함수 stopScan을 실행하게 합니다. 그럼 이제 stopScan함수를 다음과 같이 생성합니다. 코딩할 부분은 MainActivity 클래스 내부입니다.

public class MainActivity extends AppCompatActivity {
{
   (생략)
    /*
    Stop scanning
     */
    private void stopScan() {
        // check pre-conditions
        if( is_scanning_ && ble_adapter_ != null && ble_adapter_.isEnabled() && ble_scanner_ != null ) {
            // stop scanning
            ble_scanner_.stopScan( scan_cb_ );
        }
        // reset flags
        scan_cb_= null;
        is_scanning_= false;
        scan_handler_= null;
        // update the status
        tv_status_.setText( "scanning stopped" );
    }
}

BluetoothLeScanner의 stopScan 함수를 호출하기 전에 몇가지 조건들을 체크합니다.

  • 현재 스캐닝 중인가?
  • ble 어댑터와 스캐너는 생성되어 있는가?
  • ble는 활성화 되어 있는가?
    위 조건이 만족되면 BluetoothLeScanner의 stopScan이 호출되어 스캔이 멈추게 됩니다.

장치 연결 준비

스캔할 때 특정 MAC 주소를 가지는 장치만 찾도록 했습니다. 그럼 이제 이 장치에 연결하는 코드를 구현해 보겠습니다.
접속하는 코드는 어디에 구현하면 좋을까요? 방금전에 코딩한 stopScan 함수에 구현하면 좋습니다. 스캔이 종료되고, 찾아진 장치의 정보는 스캔 콜백에 의해 scan_results_에 저장되게 됩니다.
먼저 다음과 같이 stopScan함수에 스캔이 종료되었을 때 호출되는 scanComplete함수를 추가합니다.

    private void stopScan() {
        // check pre-conditions
        if( is_scanning_ && ble_adapter_ != null && ble_adapter_.isEnabled() && ble_scanner_ != null ) {
            // stop scanning
            ble_scanner_.stopScan( scan_cb_ );
            scanComplete();
        }
(생략)

그럼 이제 scanComplete함수에서 찾아진 장치에 연결하는 코드를 다음과 같이 MainActivity 내부에 구현합니다.

public class MainActivity extends AppCompatActivity {
{
   (생략)
/*
    Handle scan results after scan stopped
     */
    private void scanComplete() {
        // check if nothing found
        if( scan_results_.isEmpty() ) {
            tv_status_.setText( "scan results is empty" );
            Log.d( TAG, "scan results is empty" );
            return;
        }
        // loop over the scan results and connect to them 
        for( String device_addr : scan_results_.keySet() ) {
            Log.d( TAG, "Found device: " + device_addr );
            // get device instance using its MAC address
            BluetoothDevice device= scan_results_.get( device_addr );
            if( MAC_ADDR.equals( device_addr) ) {
                Log.d( TAG, "connecting device: " + device_addr );
                // connect to the device
                connectDevice(device);
            }
        }
    }
}

scanComplete는 다음과 같은 작업을 합니다.

  • scan_results_에 값이 있는지 검사
  • 값이 있다면 하나씩 추출하여 연결 시도
  • 이 때, 지정된 MAC 주소를 다시 확인한 후 접속 시도

startScan할 때 지정된 MAC 주소를 갖는 장치만 스캔하게 했지만, 여기서 한번 더 확인해 줍니다.

드디어 이제 장치에 연결하는 것만 남았습니다.

BLE 장치 연결 구현

다음과 같이 GATT서버에 접속하기 위한 멤버변수와 connectDevice함수를 MainActivity 내부에 구현합니다. GATT 서버는 Peripheral 장치에서 제공됩니다. 따라서 여기서는 별도의 구현은 없습니다.

public class MainActivity extends AppCompatActivity {
{
   (생략)
    // BLE Gatt
    private BluetoothGatt ble_gatt_;
   (생략)
    /*
    Connect to the ble device
    */
    private void connectDevice( BluetoothDevice _device ) {
       // update the status
        tv_status_.setText( "Connecting to " + _device.getAddress() );
        GattClientCallback gatt_client_cb= new GattClientCallback();
        ble_gatt_= _device.connectGatt( this, false, gatt_client_cb );
    }
}

위 코드를 보면, GATT 서버에 접속할 때, GATT 클라이언트 콜백이 사용되고 있습니다. 이를 살펴보기 전에 connectGatt함수에서 두 번째 인자를 한 번 짚어 보겠습니다. 이 인자는 자동으로 연결을 시도하는지를 설정하는 것입니다. 여기서는 false를 입력하여 자동으로 연결하기 않게 했습니다. 자동 연결이 오동작하거나 원치않게 연결되면 배터리 소모가 많아지기 때문에 주의가 필요합니다. 따라서 여기서는 자동연결하지 않도록 설정합니다.

Gatt 클라인트 콜백 구현

스캔된 장치에 연결하기 위해, 즉 장치가 제공하는 GATT 서버에 접속하기 위한 클라이언트 콜백이 필요합니다. 먼저 연결이 제대로 되는지 결과를 확인하기 위한 onConnectionStateChange함수를 먼저 구현합니다. 아래의 GattClientCallback 클래스도 MainActivity 내부에 구현합니다.

public class MainActivity extends AppCompatActivity {
{
   (생략)
/*
    Gatt Client Callback class
    */
    private class GattClientCallback extends BluetoothGattCallback {
        @Override
        public void onConnectionStateChange( BluetoothGatt _gatt, int _status, int _new_state ) {
            super.onConnectionStateChange( _gatt, _status, _new_state );
            if( _status == BluetoothGatt.GATT_FAILURE ) {
                disconnectGattServer();
                return;
            } else if( _status != BluetoothGatt.GATT_SUCCESS ) {
                disconnectGattServer();
                return;
            }
            if( _new_state == BluetoothProfile.STATE_CONNECTED ) {
                // update the connection status message
                tv_status_.setText( "Connected" );
                // set the connection flag
                connected_= true;
                Log.d( TAG, "Connected to the GATT server" );
            } else if ( _new_state == BluetoothProfile.STATE_DISCONNECTED ) {
                disconnectGattServer();
            }
        }
    }
}

연결 상태가 변경되면 위에서 구현한 onConnectionStateChange함수가 호출됩니다. 연결이 성공하면 먼저 상태창 메시지로 확인만 하도록 하겠습니다. 나중에 필요한 기능을 추가할 것입니다. 그런데 연결이 실패했다면 GATT 서버 연결을 종료하도록 해야 합니다. 서버 연결을 종료하기 위해 다음과 같이disconnectGattServer 함수 구현이 필요합니다.

public class MainActivity extends AppCompatActivity {
{
   (생략)
/*
    Disconnect Gatt Server
    */
    public void disconnectGattServer() {
        Log.d( TAG, "Closing Gatt connection" );
        // reset the connection flag
        connected_= false;
        // disconnect and close the gatt
        if( ble_gatt_ != null ) {
            ble_gatt_.disconnect();
            ble_gatt_.close();
        }
    }
}

마지막으로, startScan함수 초입에 기존에 연결된 Gatt 서버와의 연결을 종료하도록`disconnectGattServer() 함수를 호출하도록 합니다.

    private void startScan( View v ) {
    (생략)
        // 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;
        }
        // disconnect gatt server
        disconnectGattServer();

스캔한 장치에 연결하기 위한 모든 구현이 끝났습니다. 지금은 단순히 연결만 해보는 것입니다. 연결 후 명령 전송이나 데이터 수신은 다음에 다룰 것입니다.

연결 결과 확인

이제 안드로이드 스튜디오에서 Build -> Make Project를 실행합니다. 문제 없다면 앱을 스마트폰에 설치합니다.

한가지 특이한 현상이 발견됩니다. 이전글까지 구현상태로는 지정된 장치 스캐닝이 매번 잘 됐는데, 스캔을 종료하는 핸들러를 추가한 이후로는 장치가 바로 바로 스캔되지 않는 결과가 종종 나타납니다. 아래와 같이 아무런 스캔 결과가 없다고 말이죠.
image.png

stopScan을 호출하는 핸들러 부분을 커멘트하고 앱을 실행시키면 매번 해당 장치 스캔이 잘 됩니다. 왜 핸들러 추가했을 때, 장치 스캔이 가끔 안되는지는 저도 모르겠습니다. 지금으로써는 장치 검색이 안되면 여러번 스캔 버튼을 누르는 수밖에 없네요.

일단 stopScan을 관리하는 핸들러를 추가하고 앱을 실행시킵니다. 몇 번 스캔 버튼을 클릭하면 장치가 검색이 됩니다. 그런데 저는 장치 연결 후 아래와 같이 에러가 발생했습니다.
image.png
image.png

에러를 살펴보니 View 출력하는데 뭔가 문제가 있는것처럼 보입니다. View를 생성한 쓰레드만이 View를 컨트롤 해야 한다 그런 이유 같습니다. 그래서 GattClientCallback에서 연결이 성공적일 때 TextView에 출력하라는 메시지를 지우니 아래와 같이 연결이 성공적으로 됩니다.
image.png

연결 상태를 화면에 출력하는 부분은 다음에 해봐야겠습니다. Toast 메시지도 오류가 발생합니다.

장치로 연결 상태 확인

장치에서 연결이 변경되면 어떻게 되는지 확인해 봅니다.

  • 연결 전
    image.png
  • 연결 후
    image.png
    제대로 장치가 원격으로 연결되었다는 메시지가 출력됩니다. 기쁩니다!
    참고로, 장치에서 블루투스 보안 설정이 되어 있다면 해제를 해야 합니다.

다음은 간단히 장치에 제어 명령을 전송하는 것을 구현해 보겠습니다.

오늘의 실습: 스스로 작성한 앱으로 Peripheral 장치에 연결해 보세요.

Coin Marketplace

STEEM 0.30
TRX 0.12
JST 0.034
BTC 64231.88
ETH 3128.59
USDT 1.00
SBD 3.95