라즈베리파이 안드로이드 앱 통신 - lajeubelipai andeuloideu aeb tongsin

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

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 는 어디에서 온 녀석일까요? 하기 경로에서 가지고 왔습니다. 자주 사용하는 기능들을 간단하게 정리해 놓은 코드입니다.

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

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

건투를 빕니다!

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

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

Toplist

최신 우편물

태그