메뉴 건너뛰기

모바일앱


이 연재는 아래의 방법들을 설명합니다.

 

1. 라즈베리파이에서 소형 진동모터를 제어하는 방법
2. 라즈베리파이에서 BLE를 동작시키고 다른 디바이스에서 검색되도록 하는 방법
3. 안드로이드에서 라즈베리파이의 BLE(Advertising)신호를 검색하고 연결한 후 데이터를 전송하는 방법

 

현재 안드로이드 마켓에서 배포되고 있고 개발중인 "두근두근"이라는 앱에 블루투스 기능을 추가했던 내용을 기록하였습니다.

정확하게는, 앱과 라즈베리파이를 블루투스로 연결하였고 라즈베리파이에 붙어 있는 진동모터를 동작시키도록 개발한 내용을 정리하였습니다.

 

안드로이드에서 라즈베리파이의 BLE(Advertising)신호를 검색하고 연결한 후 데이터를 전송하는 방법


- 먼저,  Androidmanifest.xml 파일에 아래와 같은 권한과 필요한 기능을 추가합니다.

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />



- 코드에서도 디바이스가 블루투스 기능을 지원하는지 확인합니다.  지원하지 않을 경우 이 예제코드는 걍 앱을 종료 합니다.

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //블루투스 지원이 안되면 걍 종료
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            finish();
            return;
        }


- 이제 불루투스 모듈을 다루기 위한 BluetoothAdapter를 확보합니다. 

BluetoothManager bleManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        bleAdapter_ = bleManager.getAdapter();


- 버튼을 두개만 만들었습니다. 첫 버튼은 연결을 하거나 종료할때 사용하고 두번째 버튼은 명령을 보낼때 사용합니다.  그래서 두번째 버튼은 연결이 되어 있는 상태에서만 사용이 가능하도록 코드를 작성해야 했고 첫번째 버튼은 연결을 시도하기 위해 블루투스 장치를 스캔하고 있거나, 연결이 된 상태일때등을 감안해서 기능을 바꾸어야 했기에 코드가 약간 복잡합니다ㅎ


connectBtn = findViewById(R.id.connectBtn);
        connectBtn.setText("Start scan");
        connectBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                
                if (bIsScanning_ == true) {
                    //스캔중인데 버튼을 누르면 스캔을 종료하도록
                    stopScan();
                }
                else if (bIsConnected_ == true){
                    //연결중인데 버튼을 누르면 연결을 끊도록
                    disconnectGattServer();
                }
                else if (bIsScanning_ == false) {
                    //연결상태도 아니고 스캔상태도 아닌데 버튼을 누르면 블루투스 장치의 스캔을 시작하도록
                    startScan();
                }
            }
        });

        sendBtn = findViewById(R.id.sendBtn);
        sendBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                sendData();
            }
        });
        sendBtn.setEnabled(false);

- 먼저 startScan 코드를 살펴 보겠습니다. 블루투스 모듈을 다루기 위한 bleAdater 가 잘 확보되어 있는지를 확인합니다. 만약 확보에 실패하면 BLE장치를 끄고 켜는 설정화면으로 유도합니다.

if (bleAdapter_ == null || !bleAdapter_.isEnabled()) {
            enableBLE();
            return;
        }
:
:

private void enableBLE() {
        //블루투스 설정 화면으로 이동
        Intent ble_enable_intent= new Intent( BluetoothAdapter.ACTION_REQUEST_ENABLE );
        startActivityForResult( ble_enable_intent, REQUEST_ENABLE_BT );
}


- 그리고 뒤에 안 사실인데, 안드로이드의 롤리팝 버전 이후부터는 BLE 스캔 기능이 위치정보와 관련이 있어서 인지 위치정보 사용 권한의 허락을 받아야 장치가 스캔이 된다고 하네요. 그래서 아래와 같이 해당 권한을 확인하고 권한이 없으면 요청하는 코드가 존재합니다.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                requestLocationPermission();
                return;
            }
        }


- 이제 본격적으로 BLE장치의 스캔에 들어갑니다.  UUID_DKDK_SERVICE 값은 이전 연재에서 사용하였던 UUID 값을 그대로 사용합니다.  주변에 이미 수많은 BLE장치들이 신호를 내고 있을지 모릅니다. (저같은 경우는 윗집, 옆집, 아랫집의 각종 장치들과 우리집 TV와 스마트 워치까지 무진장 많은 장치들이 신호를 내고 있더군요.) 그래서, 그들 중 내가 찾고자 하는 디바이스만 나열하도록 이 UUID_DKDK_SERVICE값을 필터 값으로 넣고 장치의 스캔을 시도합니다.


List<ScanFilter> filters= new ArrayList<>();
        ScanFilter scan_filter= new ScanFilter.Builder()
                .setServiceUuid( new ParcelUuid( UUID_DKDK_SERVICE ) ) //필터 값으로 고유 UUID값을 넣고
                .build();
        filters.add( scan_filter );

        ScanSettings settings= new ScanSettings.Builder()
                .setScanMode( ScanSettings.SCAN_MODE_LOW_POWER )
                .build();

        deviceList_ = new ArrayList<>();
        scanCb_ = new BLEScanCallback(deviceList_);
        bleScanner_ = bleAdapter_.getBluetoothLeScanner();
        bleScanner_.startScan( filters, settings, scanCb_);

        setConnectBtn("Stop scanning", true); //스캔이 시작되면 첫번째 버튼의 표시 문구를 바꾸고

        scanHandler_ = new Handler();
        scanHandler_.postDelayed(new Runnable() {
            @Override
            public void run() {
                stopScan();
            }
        }, SCAN_PERIOD ); //약 7초간 스캔을 하면 자동으로 스캔 활동을 종료 하게끔 설정

        bIsScanning_ = true;

- 스캔을 하다가 장치가 발견되면 BLEScanCallback 클래스의 콜백함수가 호출이 됩니다. 이 클래스에 담겨 있는 콜백함수들의 모습과 클래스의 모습은 아래와 같습니다.

private class BLEScanCallback extends ScanCallback {
        private ArrayList<BluetoothDevice> _foundDevices;

        BLEScanCallback( ArrayList<BluetoothDevice>  _scanDeviceList ) {
            _foundDevices = _scanDeviceList;
        }

        @Override
        public void onScanResult( int _callback_type, ScanResult _result ) {
            addScanResult( _result );

            new Handler().postDelayed( new Runnable() {
                @Override
                public void run() {
                    scanFinished();
                }
            }, 100 );
        }

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

            new Handler().postDelayed( new Runnable() {
                @Override
                public void run() {
                    scanFinished();
                }
            }, 100 );
        }

        @Override
        public void onScanFailed( int _error ) {
            Log.e( TAG, "Scan failed : " +_error );
        }

        private void addScanResult( ScanResult _result ) {
            BluetoothDevice device= _result.getDevice();
            _foundDevices.add(device );
        }
    }


즉, 찾아낸 디바이스를 담을 리스트를 던져 주면, 스캔되어 나오는 디바이스를 해당 리스트에 담는 행동을 하는게 다 입니다. 


- 장치의 스캔이 종료되면 아래와 같이 scanFinished() 코드가 100밀리초 후에 호출이 되게끔 작성했습니다.

new Handler().postDelayed( new Runnable() {
                @Override
                public void run() {
                    scanFinished();
                }
            }, 100 );


- scanFinished 코드에서는 콜백클래스에 전달했던 deviceList_ 리스트에 뭐좀 낚여 있는지를 확인하고- 있으면 해당 리스트를 순회하면서 낚여 있는 장치들의 정보를 Log로 표시하고 그중 가장 첫번째 deivce에 연결을 시도합니다. (연결을 시도하자마자 루프를 빠져 나옵니다)

if( deviceList_.isEmpty() ) {
            setConnectBtn("Start scanning", true);
            return;
        }


        for( BluetoothDevice _device : deviceList_) {

            ParcelUuid[] uuids = _device.getUuids();

            if (uuids != null)
                for (ParcelUuid uuid : uuids) {
                    Log.d( TAG, "device uuid: " + uuid.toString() );
                }

            if (_device.getAddress() != null)
                Log.d( TAG, "device address: " + _device.getAddress() );

            if (_device.getName() != null)
                Log.d( TAG, "device address: " + _device.getName() );

            connectDevice(_device);
            return;

        }

- 디바이스에 연결을 요청하는 방법은 안드로이드 마쉬멜로우 버전부터 약간 차이가 있습니다. 즉, 디바이스에 연결을 요청하는 api의 마지막 파라메터가 문제인데요,


GattClientCallback gattClientCb = new GattClientCallback();
        Log.d( TAG, "Try to connect " + _device.getName() );

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            bleGatt_= _device.connectGatt( this, false, gattClientCb, BluetoothDevice.TRANSPORT_LE);
        }
        else {
            bleGatt_= _device.connectGatt( this, false, gattClientCb);
        }

실은 _device.connectGatt 에 마지막 파라메터를 넣지 않아도 안드로이드의 버전에 상관없이 api가 호출이 됩니다. 하지만, (M 버전 이상인)제 스마트폰에서 마지막 파라메터 없이 api를 사용하니 디바이스와 연결이 되지 않더군요. 그리고 마지막 파라메터는 마쉬멜로우 버전 이상에서 새로 생겼습니다. 이에, M버전 이하에서는 어쩔 수 없이 마지막 파라메터를 빼고 이  api를 호출해야 하며, 이렇게 해서 디바이스와 연결이 잘 되는지는 확인을 못해 보았습니다. (혹, 해당 버전에서 연결이 성공하신 분이 있으시다면 댓글 부탁드립니다^_^)


- 연결을 시도하면, 아래와 같이 만들어서 connectGatt  api에 파라메터로 넘겼던 콜백클래스 속 콜백함수가 호출이 됩니다.

GattClientCallback gattClientCb = new GattClientCallback();

- 해당 콜백클래스의 속을 보겠습니다. onConnectionStateChange 라는 콜백이 보입니다. 연결에 성공하면, 콜백의 파라메터 중 하나인 _new_state 의 값이 BluetoothProfile.STATE_CONNECTED 으로 채워집니다. 


private class GattClientCallback extends BluetoothGattCallback {
        @Override
        public void onConnectionStateChange( BluetoothGatt _gatt, int _status, int _new_state ) {
            super.onConnectionStateChange( _gatt, _status, _new_state );
            if( _new_state == BluetoothProfile.STATE_CONNECTED ) {
                bIsConnected_ = true;
                Log.d( TAG, "Connected to the GATT server" );
                _gatt.discoverServices();
            } else if ( _new_state == BluetoothProfile.STATE_DISCONNECTED ) {
                Log.d( TAG, "status is STATE_DISCONNECTED" );
                disconnectGattServer();

                setConnectBtn("Start scanning", true);
                setCmdBtn("Send cmd", false);
            }
        }

        @Override
        public void onServicesDiscovered( BluetoothGatt _gatt, int _status ) {
            super.onServicesDiscovered( _gatt, _status );

            if( _status != BluetoothGatt.GATT_SUCCESS ) {
                Log.e( TAG, "Discovery failed, status: " + _status );
                disconnectGattServer();
                setConnectBtn("Start scanning", true);
                setCmdBtn("Send cmd", true);
                return;
            }

            List<BluetoothGattCharacteristic> matching_characteristics = BluetoothUtils.findBLECharacteristics( _gatt );
            if( matching_characteristics.isEmpty() ) {
                Log.e( TAG, "failed to find characteristic" );
                setConnectBtn("Start scanning", true);
                setCmdBtn("Send cmd", true);
                return;
            }

            Log.d( TAG, "Services discovery : success" );

            setConnectBtn("Disconnect", true);
            setCmdBtn("Send cmd", true);
        }

        @Override
        public void onCharacteristicChanged( BluetoothGatt _gatt, BluetoothGattCharacteristic _characteristic ) {
            super.onCharacteristicChanged( _gatt, _characteristic );

        }

        @Override
        public void onCharacteristicWrite( BluetoothGatt _gatt, BluetoothGattCharacteristic _characteristic, int _status ) {
            super.onCharacteristicWrite( _gatt, _characteristic, _status );
            if( _status == BluetoothGatt.GATT_SUCCESS ) {
                Log.d( TAG, "onCharacteristicWrite : SUCCESS" );
            } else {
                Log.d( TAG, "onCharacteristicWrite : FAILED" );
                //disconnectGattServer();
            }
        }

        @Override
        public void onCharacteristicRead(BluetoothGatt _gatt, BluetoothGattCharacteristic _characteristic, int _status) {
            super.onCharacteristicRead(_gatt, _characteristic, _status);
            if (_status == BluetoothGatt.GATT_SUCCESS) {
                Log.d( TAG, "onCharacteristicRead : SUCCESS" );
                //readCharacteristic(characteristic);
            } else {
                Log.e( TAG, "Characteristic read unsuccessful, status: " + _status);

            }
        }

        private void readCharacteristic( BluetoothGattCharacteristic _characteristic ) {
            byte[] msg= _characteristic.getValue();
            Log.d( TAG, "read: " + msg.toString() );
        }
    }


BluetoothProfile.STATE_CONNECTED 값을 받았다면, 이제 디바이스와 연결이 되었습니다. 디바이스가 어떤 기능(Characteristic)들이 있는지를 찾아내기(Discover) 위해 discoverServices()를 호출합니다. 그리고, discoverService 호출이 성공했다면, 바로 아래쪽,  OnServicesDiscovered 콜백이 호출되면서 _status 값에 BluetoothGatt.GATT_SUCCESS 값이 채워져 옵니다. 자, 이제 해당 디바이스에게 명령을 보내거나 또는 정보를 받을 수 있는 기능(Characteristic)이 있는지 확인해 보겠습니다.


List<BluetoothGattCharacteristic> matching_characteristics = BluetoothUtils.findBLECharacteristics( _gatt );
            if( matching_characteristics.isEmpty() ) {
                Log.e( TAG, "failed to find characteristic" );
                disconnectGattServer();
                setConnectBtn("Start scanning", true);
                setCmdBtn("Send cmd", true);
                return;
            }

            Log.d( TAG, "Services discovery : success" );

            setConnectBtn("Disconnect", true);
            setCmdBtn("Send cmd", true);


가장 첫 라인에서 보이는 것 처럼 듣보잡 유틸리티에게 디바이스 정보를 던져 주고 기능(Characteristic) 목록을 얻어 옵니다. 만약, 하나도 없다면 이 친구랑 뭔가 할게 없기 때문에 걍 연결을 종료 합니다. 하지만, 뭔가 있다면! 아래와 같이 해당 기능으로 명령을 보내 보겠습니다. 그리고 듣보잡 유틸리티는 따로 설명을 드리도록 하겠습니다.


if( !bIsConnected_)
        {
            Log.e( TAG, "Failed to sendData" );
            return;
        }

        BluetoothGattCharacteristic _cmdCharacteristic= BluetoothUtils.findCommandCharacteristic( bleGatt_ );

        if( _cmdCharacteristic == null ) {
            disconnectGattServer();
            return;
        }

        byte[] cmds= new byte[6];
        cmds[0]= 0;
        cmds[1]= 0;
        cmds[2]= 0;
        cmds[3]= 0;
        cmds[4]= 0;
        cmds[5]= 0;

        if (bIsMotorOn_ == true) {
            cmds[0]= 1; // 모터 끄기
            setCmdBtn("Motor On", true);
        }
        else {
            setCmdBtn("Motor Off", true);
        }

        _cmdCharacteristic.setValue( cmds );

        if( bleGatt_.writeCharacteristic( _cmdCharacteristic ) ) {
            Log.d( TAG, "Successfully sent data." );
            bIsMotorOn_ = !bIsMotorOn_;
        }
        else
        {
            Log.e( TAG, "Failed to send data" );
            bIsMotorOn_ = false;
            disconnectGattServer();
        }


- 디바이스와 연결이 되어 있는지 확인을 하고 연결이 되어 있으면 (이전 라즈베라파이 관련 연재 기준으로) 모터를 끄고 켜는 명령을 전송합니다. 그리고 이번에도 BluetoothUtils에게 findCommandCharacteristic 라는 함수로 Command를 보낼 수 있는 기능을 묻고 얻어 옵니다. 코드에 보이는 것 처럼 총 6바이트 길이의 데이터를 전송합니다. 자, 그럼- 이 BluetoothUtils 는 어디에서 온 녀석일까요? 하기 경로에서 가지고 왔습니다. 자주 사용하는 기능들을 간단하게 정리해 놓은 코드입니다.


https://github.com/bignerdranch/android-bluetooth-testbed/blob/master/BluetoothLowEnergy/app/src/main/java/com/bignerdranch/android/bluetoothtestbed/util/BluetoothUtils.java


이로써, 안드로이드에서 블루투스 장치를 스캔하고, 연결하고, 명령을 전송하는 코드를 살펴 보았습니다. 전체 코드는 아래의 경로에 올려 두었습니다^^


건투를 빕니다!


* 코드 경로 https://github.com/gunman97/android_ble_example

* 참고로 열심히 배포중이고 개발중인 두근두근앱은 "https://top.dkdk.io"에서 확인하실 수 있습니다^_^









번호 제목 글쓴이 날짜 조회 수
공지 [TIP] 죽지 않는 안드로이드 서비스 만들기 (Unstoppable service) [6] 파이팅건맨 2015.06.26 6503
공지 [TIP] 안드로이드 앱 빌드시 "Error:Execution failed for task ':app:compileDebugJavaWithJavac'" 오류가 발생할 경우 파이팅건맨 2017.09.13 6096
공지 [TIP] 안드로이드 앱이 처음 설치될때 Referrer 정보 받아 오기 파이팅건맨 2016.08.30 3327
43 [TIP] iOS - UIWebView에 로컬 html 파일 로드하기 (Swift 4) 파이팅건맨 2019.06.05 13
42 [TIP] Tizen Push가 갑자기 내려오지 않을때 파이팅건맨 2019.05.20 18
» [TIP] 두근두근앱이 라즈베리파이를 두근거리도록 개발한 기록 #3 파이팅건맨 2019.05.15 61
40 [TIP] 두근두근앱이 라즈베리파이를 두근거리도록 개발한 기록 #2 파이팅건맨 2019.05.11 88
39 [TIP] Android 코드에서 블루투스 연결이 잘 안될때 파이팅건맨 2019.05.11 80
38 [TIP] 두근두근앱이 라즈베리파이를 두근거리도록 개발한 기록 #1 파이팅건맨 2019.05.07 113
37 [TIP] 타이젠 스튜디오에서 웨어러블 디바이스로 디버깅을 위한 바이너리 전송이 안 될때 파이팅건맨 2019.03.26 123
36 [TIP] http://tizen.org/system/tizenid 으로 타이젠 고유 id를 확보할 때 유의할 점 파이팅건맨 2019.03.14 67
35 [TIP] "cordova run android" 명령을 실행했는데 "A problem occurred evaluating project ':CordovaLib'"오류가 뜰때 파이팅건맨 2019.01.22 173
34 [TIP] Mac에서 Cordova run android 를 실행했는데 "Command failed with exit code EACCES" 오류가 뜰때 파이팅건맨 2019.01.22 128
33 [TIP] 안드로이드에서 심박수 측정하는 코드 [6] 파이팅건맨 2019.01.02 301
32 [TIP] Google Cloud API 사용시 안드로이드의 Assets 폴더에 있는 Crendential 파일 사용하기 파이팅건맨 2018.05.29 206
31 [TIP] 안드로이드 스튜디오에서 Error:android-apt plugin is incompatible with the Android Gradle plugin. Please use 'annotationProcessor' configuration instead 오류날때 파이팅건맨 2018.02.02 1738
30 [TIP] 안드로이드 앱 빌드시 "Error:Execution failed for task ':app:compileDebugJavaWithJavac'" 오류가 발생할 경우 파이팅건맨 2017.09.13 6096
29 [TIP] Error:java.lang.OutOfMemoryError: GC overhead limit exceeded 파이팅건맨 2016.11.03 864
28 [TIP] 안드로이드 앱이 처음 설치될때 Referrer 정보 받아 오기 파이팅건맨 2016.08.30 3327
27 [TIP] 안드로이드에서 구글 스프레드시트에 데이터 쓰기 파이팅건맨 2016.05.04 2427
26 [TIP] 안드로이드에서 키보드가 나타날 때 레이아웃이 위로 움직인다면 파이팅건맨 2016.03.22 811
25 [TIP] 안드로이드 - 설치된 앱 목록 얻기 파이팅건맨 2016.03.22 2947
24 [TIP] 안드로이드의 최상단에 띄워놓은 Floating Window가 키보드를 인지하는 방법 파이팅건맨 2016.01.07 1638
위로