안드로이드 BLE 통신
1. 블루투스 권한 설정
안드로이드에서 BLE 통신을 사용하기 위해서는
APP 이 특정 권한들을 획득해야 한다.
아래와 같이 AndroidManifast.xml 파일에 권한 코드를 추가한다.
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
BLE 통신을 위해 BLUETOOTH, BLUETOOTH_ADMIN 권한 외에
ACCESS_COARSE_LOCATION라는 위치 권한도 필요하다.
BLE 장치가 위치 정보를 제공하는 것이 많기 때문에
구글에서 위치 권한도 요청하기로 했다고 한다.
안드로이드 마시멜로 버전 이상부터는
사용자에게 요청하여 권한을 획득해야 한다.
// OS 버전이 마시멜로 이상이라면 위치 권한이 있는지 점검 후 없다면 사용자에게 위치 권한을 요청
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 위치 권한이 있는지 점검
// 없다면 -1 반환
int permission = checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION);
// 권한이 없을 때
if (permission == PackageManager.PERMISSION_DENIED)
{
String[] permissions = new String[1];
permissions[0] = android.Manifest.permission.ACCESS_COARSE_LOCATION; // 사용자에게 요청할 권한
requestPermissions(permissions, LOCATION_PERMISSION_REQUEST_CODE); // 사용자에게 권한 요청
}
// 권한이 있을 때
else
{
// 변수 값을 true 로 변경
isPermissionAllowed = true;
}
}
권한을 요청한 후 사용자의 응답에 따라
처리가 가능하도록 메서드를 추가한다.
본 예제는 권한을 득하면 바로 주변의
BLE 기기를 스캐닝 하도록 구성되어 있다.
// 사용자에게 권한을 요청한 결과가 들어오는 메소드
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
// 위치 권한 요청의 응답값인지 체크
if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
// 권한을 획득
if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
{
// BLE 스캐닝
scanLeDevice(true);
}
// 권한 획득 실패
else
{
Toast.makeText(this, R.string.error_permission_denied, Toast.LENGTH_SHORT);
finish();
}
}
}
2. BLE SCAN
BLE 스캐닝 시 배터리 소모가 극심하기 때문에
로직을 구현할 때 두가지 권고사항이 있다.
1.원하는 기기 발견 시, 스캐닝 중지
2.스캐닝을 루프시키지 말 것.
위의 두 가지 사항에 따라 설정한 시간이
지난 후 스캐닝 후 중지되도록 구현해야 한다.
private void scanLeDevice(final boolean enable) {
if (enable) {
// SCAN_PERIOD 값만큼 시간이 지나면 스캐닝 중지
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
stopScanBLE();
invalidateOptionsMenu();
}
}, SCAN_PERIOD);
mScanning = true;
startScanBLE();
} else {
mScanning = false;
stopScanBLE();
}
invalidateOptionsMenu();
}
// BLE 스캔시작
private void startScanBLE(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
}
else
{
mBluetoothAdapter.startLeScan(mLeScanCallback);
}
}
// BLE 스캔중지
private void stopScanBLE(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
}
else
{
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}
BLE 기기가 스캔되면 콜백함수가 호출된다.
여기서 찾은 기기를 리스트 객체에 추가하고, 화면을 갱신한다.
// BLE 기기가 스캔되면 호출. 롤리팝 이하 버전
private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mLeDeviceListAdapter.addDevice(device);
mLeDeviceListAdapter.notifyDataSetChanged();
}
});
}
};
// BLE 기기가 스캔되면 호출. 롤리팝 이상 버전
private ScanCallback mScanCallback = new ScanCallback() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
BluetoothDevice device = result.getDevice();
mLeDeviceListAdapter.addDevice(device);
mLeDeviceListAdapter.notifyDataSetChanged();
}
};
3. BLE CONNECT
검색된 BLE 리스트에서 특정 기기를 선택하면
mBluetoothLeService 의 connect 메소드를
호출하여 기기에 연결을 시도한다.
private BluetoothGatt mBluetoothGatt;
private BluetoothGattCharacteristic mReadCharacteristic = null;
private BluetoothGattCharacteristic mWriteCharateristic = null;
private final String SERVICE = "0000fff0-0000-1000-8000-00805f9b34fb";
private final String WRITE_UUID = "0000fff1-0000-1000-8000-00805f9b34fb";
private final String READ_UUID = "0000fff2-0000-1000-8000-00805f9b34fb";
private boolean findGattServices() {
List<BluetoothGattService> gattServices = mBluetoothGatt.getServices();
if (gattServices == null) return false;
mReadCharacteristic = null;
mWriteCharateristic = null;
for (BluetoothGattService gattService : gattServices){
HashMap<String, String> currentServiceData = new HashMap<String, String>();
if(gattService.getUuid().toString().equals(SERVICE)){
List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics();
// Loops through available Characteristics.
for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics){
if(gattCharacteristic.getUuid().toString().equals(READ_UUID)){
final int charaProp = gattCharacteristic.getProperties();
if((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0){
try{
mReadCharacteristic = gattCharacteristic;
List<BluetoothGattDescriptor> list = mReadCharacteristic.getDescriptors();
Log.d(TAG, "read characteristic found : " + charaProp);
mBluetoothGatt.setCharacteristicNotification(gattCharacteristic, true);
//리시버 설정
BluetoothGattDescriptor descriptor = mReadCharacteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
catch (Exception e){
e.printStackTrace();
return false;
}
}
else{
Log.d(TAG, "read characteristic prop is invalid : " + charaProp);
}
}
else if(gattCharacteristic.getUuid().toString().equalsIgnoreCase(WRITE_UUID)){
final int charaProp = gattCharacteristic.getProperties();
if((charaProp | BluetoothGattCharacteristic.PROPERTY_WRITE) > 0){
Log.d(TAG, "write characteristic found : " + charaProp);
mWriteCharateristic = gattCharacteristic;
}
else{
Log.d(TAG, "write characteristic prop is invalid : " + charaProp);
}
}
}
}
}
return true;
}
BLE 기기는 특정 기능을 보유하고 있는 특성(Characteristic)과
그 특성들의 집합인 서비스(Service)로 구성되어 있다.
기기의 데이터를 읽고 쓰는 작업을 할 때도 이 특성들을 사용한다.
기기와 연결을 성공하면 해당 기기의 서비스와 특성들을 찾아
사용할수 있도록 등록해야 한다.
특성의 등록이 완료되면 기기 연결의 성공여부를
onConnectionStateChange() 메서드에서 받을 수 있다.
4. BLE 데이터 통신
연결이 성공하면 기기와 데이터를 주고받을 수 있다.
기기로 전송할 Command를 가공하여 실행하면 된다.
public void writeData(byte[] data){
try{
sendData = null;
boolean result = false;
if(mBluetoothGatt.connect())
{
if(mWriteCharateristic == null){
Log.i(TAG, "Write gatt characteristic is null");
} else if(mReadCharacteristic == null){
Log.i(TAG, "Read gatt characteristic is null");
} else {
int dataLen = data.length;
String sendDataG = "";
for(int i=0; i<data.length; i++){
sendDataG += String.format("%02x", data[i]);
}
System.out.println("BLE Command 데이터 : " + sendDataG);
sendData = data;
Handler hd = new Handler();
hd.postDelayed(new Runnable() {
@Override
public void run() {
mWriteCharateristic.setValue(sendData);
if(mWriteCharateristic == null){
Log.d("Ble UUID가 없습니다.", "");
mBluetoothGatt.disconnect();
}else {
mBluetoothGatt.writeCharacteristic(mWriteCharateristic);
}
}
},5);
}
}
else
{
Log.i(TAG, "Bluetooth gatt is not connected");
}
}
catch (Exception e)
{
Log.e("", e.toString());
}
}
기기에 Command를 전송하였을때 Response 데이터가 있다면
응답은 onCharacteristicChanged() 메서드로 콜백된다.
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
.
.
.
.
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
try
{
byte[] readByte = characteristic.getValue();
if(mBlelistner != null)
mBlelistner.recvData(readByte); // 기기 응답을 받는 리스너
}
catch(Exception e)
{
Log.d(TAG, e.toString());
}
}
}
getValue() 값이 기기의 Response 데이터이다.
응답을 처리할 리스너를 등록하여 데이터를 처리한다.