Development record of developer who study hard everyday.

레이블이 안드로이드블루투스권한인 게시물을 표시합니다. 모든 게시물 표시
레이블이 안드로이드블루투스권한인 게시물을 표시합니다. 모든 게시물 표시
, , , , , , , ,

안드로이드 RxBle 사용하기

 안드로이드 RxBle 사용하기

안드로이드 개발 블로그

회사에서 BLE연결을 통해서 앱을 만들어야하는 프로젝트가 있었다.

이전에 전임자가 만들었던 앱에서 RxBle를 사용하는 것을 보고 흥미를 느껴서 나도 써보기로 했다.

내가 참고한 RxBle 공식문서는 아래와 같다.

RxBle 공식 라이브러리 문서 링크

1. 그래들

build.gradle 파일에 RxBle 라이브러리를 추가해준다.

dependencies {
//Rxble
implementation "com.polidea.rxandroidble2:rxandroidble:1.15.1"

// Rx
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
}

RxBle는 RxJava를 활용하기때문에 당연히 RxJava도 추가해주어야한다.


2. BLE 권한

BLE 기능을 사용하기 위해서는 먼저 권한을 설정해주어야한다.

근데 이 권한이 안드로이드 12부터 달라져서 os 버전별로 대응해주어야한다.

이 글의 핵심은 RxBle로 스캔, 연결, 통신하는 것이기 때문에 권한에 대해서는 간략하게 설명하고 넘어가겠다.

자세한 설명은 이전에 작성한 글을 참고하기 바란다.

안드로이드 12 블루투스 권한 자세히 알아보기

안드로이드 12 이전에는 BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION을 설정해주면 된다.

위 3가지 권한 중 ACCESS_FINE_LOCATION은 런타임 권한이라서 앱 실행시에 권한요청을 해주면 된다.

하지만 안드로이드 12부터는 BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT 권한이 추가되었다.

또한 위 3가지 권한을 런타임 시에 권한요청을 해주어야한다.

3. RxBleClient 객체 얻기

우선, 나는 BleManager라는 클래스를 만들어서 RxBle를 활용한 여러 기능들을 모아두려고 한다.

그리고 BleManager 생성자에서 RxBleClient 객체를 초기화해준다.

public class BleManager {

public static final String TAG = BleManager.class.getSimpleName();

Context mContext;
RxBleClient rxBleClient;
RxBleDevice bleDevice;

@Inject
public BleManager(@ApplicationContext Context context) {
mContext = context;
rxBleClient = RxBleClient.create(mContext);
}
}

4. 스캔

public Observable<List<BluetoothDevice>> scanDevice()  {
bleDeviceList.clear();

ScanSettings settings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
ScanFilter filter = new ScanFilter.Builder().build();

Disposable scanSubscription = rxBleClient.observeStateChanges().startWith(rxBleClient.getState())
.compose(clientStateCompose)
.flatMap(notUse -> rxBleClient.scanBleDevices(settings, filter))
//.doOnNext(scanResult -> Log.d(TAG, "doOnNext, scanDevice: " + scanResult.toString()))
.map(ScanResult::getBleDevice)
.distinct()
.takeUntil(Observable.timer(5, TimeUnit.SECONDS))
.toList()
.toObservable()
.compose(LoadingComposer)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(scanList -> {
Log.d(TAG, "scanDevice, " + scanList);
if (scanList == null) {
Log.d(TAG, "scanList is null");
} else if (scanList.size() == 1) {
Log.d(TAG, "scanList size: 1");
} else {
scanList.stream().map(RxBleDevice::getBluetoothDevice).forEach(bleDevice -> {
/*if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
return;
}*/
if (bleDevice.getName() != null &&
bleDevice.getName().contains("ACE") &&
!bleDeviceList.contains(bleDevice)){
bleDeviceList.add(bleDevice);
}
});
Log.d(TAG, "AceDevice List = " + bleDeviceList.get(0).getName() + " ," + bleDeviceList.get(0).getAddress());
}
}, error -> {
Log.e(TAG, "scanDevice error: ", error);
});

scanDisposable.add(scanSubscription);
return Observable.just(bleDeviceList);
}

위의 코드는 RxBle를 활용하여 Ble 기기를 스캔하는 코드이다.

ScanSettings와 ScanFilter 객체를 만들어준다.

ScanSettings.SCAN_MODE_LOW_LATENCY는 스캔시 저전력 모드로 스캔을 하게끔 해주는 속성이다.

Disposable scanSubscription = rxBleClient.observeStateChanges().startWith(rxBleClient.getState())

rxBleClient객체에서 observeStateChanges() 메소드를 실행하여 rxBleClient객체의 상태를 Observable로 받아준다.

이때 startWith 메소드를 사용하여 가장 먼저 rxBleClient의 현재 상태(getState())를 먼저 받아볼 수 있게 해준다.

.compose(clientStateCompose)

compose 메소드를 통해서 clientStateCompose 객체를 넣어준다.

clientStateCompose 의 역할은 client객체의 상태에 따라서 토스트를 띄워주는 것이다.

필수로 해야하는 것은 아닌데 스캔할 때 블루투스를 키지 않았거나 권한을 허용하지 않아서 에러가 나는 상황을 미리 체크한다고 보면 된다.

private ObservableTransformer<RxBleClient.State, RxBleClient.State> clientStateCompose =
new ObservableTransformer<RxBleClient.State, RxBleClient.State>() {
@Override
public ObservableSource<RxBleClient.State> apply(Observable<RxBleClient.State> upstream) {
return upstream.switchMap((Function<RxBleClient.State, ObservableSource<RxBleClient.State>>) state -> {
switch (state) {
case READY:
return Observable.just(state);

case BLUETOOTH_NOT_ENABLED: {
AlertUtils.toastShort(mContext, "블루투스를 켜주세요");
return Observable.error(new Exception());
}
case LOCATION_PERMISSION_NOT_GRANTED: {
AlertUtils.toastShort(mContext, "위치권한을 허용주세요");
return Observable.error(new Exception());
}
default:
return Observable.error(new Exception());
}
});
}
};
.flatMap(notUse -> rxBleClient.scanBleDevices(settings, filter))

이제 flatMap을 통해서 rxBleClient.scanBleDevices를 해준다.

flatMap을 사용한 이유는 scanBleDevices(ScanSettings, ScanFilter)를 하면 Observable<ScanResult> 객체가 반환되기 때문이다.

.map(ScanResult::getBleDevice)

이제 ScanResult 객체에서 RxBleDevice 객체만 뽑아낸다.

map을 통해서 진행하고 RxBle에서 제공하는 ScanResult.getBleDevice()함수를 메소드 레퍼런스로 사용했다.

.distinct()

distinct()로 겹치는 장치들은 제거해준다.

.takeUntil(Observable.timer(5, TimeUnit.SECONDS))

takeUntil 메소드는 매개변수로 받은 Observable이 데이터를 방출할 때까지 기존의 Observable에서 데이터를 받는다.

여기서는 5초동안 스캔이 진행된다는 의미이다.

.toList()
.toObservable()

RxBleDevice를 List에 담아서 Observable로 만들어준다.

.compose(LoadingComposer)

compose를 통해서 스캔을 하는 동안 프로그레스바 다이얼로그를 띄워주었다.

이 부분은 꼭 필수적인 부분이 아니고 다양한 구현방법이 가능하기 때문에 자세한 내용은 생략하겠다.

private final ObservableTransformer<List<RxBleDevice>, List<RxBleDevice>> LoadingComposer = new ObservableTransformer<List<RxBleDevice>, List<RxBleDevice>>() {
@Override
public ObservableSource<List<RxBleDevice>> apply(Observable<List<RxBleDevice>> upstream) {
return upstream
.doOnSubscribe((disposable)-> {
Log.d(TAG, "doOnSubscribe");
Disposable subscription = Observable.timer(500, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()).subscribe((unused) -> {
EventBus.getDefault().post(new LoadingEvent(true));
});
})
.doOnTerminate(() -> {
Log.d(TAG, "doOnTerminate");
EventBus.getDefault().post(new LoadingEvent(false));
});
}
};
.observeOn(AndroidSchedulers.mainThread())

스캔 후에 리스트를 보여주어야해서 mainThread에서 observeOn을 해준다.

.subscribe(scanList -> {
Log.d(TAG, "scanDevice, " + scanList);
if (scanList == null) {
Log.d(TAG, "scanList is null");
} else if (scanList.size() == 1) {
Log.d(TAG, "scanList size: 1");
} else {
scanList.stream().map(RxBleDevice::getBluetoothDevice).forEach(bleDevice -> {
/*if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
return;
}*/
if (bleDevice.getName() != null &&
bleDevice.getName().contains("ACE") &&
!bleDeviceList.contains(bleDevice)){
bleDeviceList.add(bleDevice);
}
});
Log.d(TAG, "AceDevice List = " + bleDeviceList.get(0).getName() + " ," + bleDeviceList.get(0).getAddress());
}
}, error -> {
Log.e(TAG, "scanDevice error: ", error);
});

subscribe 함수에서 List<RxbleDevice>를 원하는 방식으로 다루어주면 된다.

scanDisposable.add(scanSubscription);
return Observable.just(bleDeviceList);

CompositeDisposable(위 코드에서 scanDisposable)에 구독해서 생긴 scanSubscription을 add해주고 Observable<List<bleDevice>>를 return해준다.

5. 연결상태 구독

ble기기를 연결하는 앱을 만들 때 연결상태에 따라 UI를 변경하거나 특정 작업을 해주어햐할 때가 무조건 있다.

따라서 RxBle에서는 연결상태를 구독하는 함수를 제공한는데, 그게 바로 observeConnectionStateChanges() 함수이다.

먼저, 전체 코드를 먼저 공개한다.

public void subscribeConnection(RxBleDevice device){
bleDevice = device;
Disposable connectionDisposable = device.observeConnectionStateChanges()
.distinctUntilChanged()
.subscribe(rxBleConnectionState -> {
Log.d(TAG, "subscribeConnection, connectionState: " + rxBleConnectionState);
if(rxBleConnectionState == RxBleConnection.RxBleConnectionState.CONNECTED){
isConnected = true;
connectionSource.onNext(isConnected);

EventBus.getDefault().post(new DialogEvent(isConnected));
TiltmeterApp app = (TiltmeterApp) mContext;
app.mBtService.stop();
} else if(rxBleConnectionState == RxBleConnection.RxBleConnectionState.DISCONNECTED ||
rxBleConnectionState == RxBleConnection.RxBleConnectionState.DISCONNECTING){
Log.d(TAG, "subscribeConnection: connectionState: " + rxBleConnectionState);
isConnected = false;
connectionSource.onNext(isConnected);

TiltmeterApp app = (TiltmeterApp) mContext;
app.initBlueTooth();
}
}, error -> Log.e(TAG, "subscribeConnection: ", error)
);

connectDisposable.add(connectionDisposable);
}
    Disposable connectionDisposable = device.observeConnectionStateChanges()

observeConnectionStateChanges() 메소드를 통해 bleConnection 상태를 구독한다.

.distinctUntilChanged()

같은 상태는 1번만 받는다.

.subscribe(rxBleConnectionState -> {
Log.d(TAG, "subscribeConnection, connectionState: " + rxBleConnectionState);
if(rxBleConnectionState == RxBleConnection.RxBleConnectionState.CONNECTED){
isConnected = true;
connectionSource.onNext(isConnected);

EventBus.getDefault().post(new DialogEvent(isConnected));
TiltmeterApp app = (TiltmeterApp) mContext;
app.mBtService.stop();
} else if(rxBleConnectionState == RxBleConnection.RxBleConnectionState.DISCONNECTED ||
rxBleConnectionState == RxBleConnection.RxBleConnectionState.DISCONNECTING){
Log.d(TAG, "subscribeConnection: connectionState: " + rxBleConnectionState);
isConnected = false;
connectionSource.onNext(isConnected);

TiltmeterApp app = (TiltmeterApp) mContext;
app.initBlueTooth();
}
}, error -> Log.e(TAG, "subscribeConnection: ", error)
);

RxBleConnectionState가 CONNECTED일 때와 DISCONNECTED일 때를 나누어서 대응해준다.

connectDisposable.add(connectionDisposable);

구독한 disposable은 CompositeDisposable에 add해준다.

6. 연결하기

public void connect(String macAddress){
if(macAddress == null) {
Log.d(TAG, "connect: macAddress is null");
return;
}

RxBleDevice device;
if(macAddress.length() == 0){
device = bleDevice;
} else {
device = getDevice(macAddress);
}

if(device != null){
subscribeConnection(device);
}

Disposable connectionDisposable = device.establishConnection(false)
.doOnNext(rxBleConnection -> {
connection = rxBleConnection;
Log.d(TAG, "device connect: " + connection.toString());
})
.flatMap(rxBleConnection -> {
if(rxBleConnection.equals(RxBleConnection.RxBleConnectionState.CONNECTED)){
isConnected = true;
Log.d(TAG, "ble connect: " + isConnected);

} else if (rxBleConnection.equals(RxBleConnection.RxBleConnectionState.DISCONNECTED)) {
isConnected = false;
Log.d(TAG, "ble connect: " + isConnected);

}
return rxBleConnection.setupNotification(Constants.READ_CHAR_UUID);
})
.doOnNext(byteObservable -> Log.d(TAG, "setUp Notification: " + byteObservable))
.flatMap(observable -> {
return observable;
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(bytes -> {
Log.d(TAG, "Received Notification bytes " + byteArrayToHexaString(bytes));

Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_READ);
sendIntent.putExtra(Constants.SEND_MSG, new String(bytes));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
mContext.sendBroadcast(sendIntent);

}, error -> {
Log.d(TAG, "setUp Notification Error: " + error);
error.printStackTrace();
EventBus.getDefault().post(new DialogEvent(isConnected));
});

connectDisposable.add(connectionDisposable);
}
if(macAddress == null) {
Log.d(TAG, "connect: macAddress is null");
return;
}

RxBleDevice device;
if(macAddress.length() == 0){
device = bleDevice;
} else {
device = getDevice(macAddress);
}    //소스가 자바로 되어있어서 macAddress에 대한 예외처리를 해준다.
private RxBleDevice getDevice(String macAddress) {
return rxBleClient.getBleDevice(macAddress);
}

getDevice는 스캔할 시 보관한 macAddress로 ble 장치를 찾아내는 메소드다.

if(device != null){
subscribeConnection(device);
}

ble장치와 연결을 시도하기 전에 subscribeConnection을 해준다.

Disposable connectionDisposable = device.establishConnection(false)
.doOnNext(rxBleConnection -> {
connection = rxBleConnection;
Log.d(TAG, "device connect: " + connection.toString());
})

ble장치와 연결을 시도할 때는 establishConnection 메소드를 사용한다.

매개변수에는 자동연결을 할 것인지를 boolean 값으로 전달한다.

doOnNext는 그냥 디버깅을 위해서 현재 디바이스의 연결상태를 로그로 출력해보았다.

.flatMap(rxBleConnection -> {
if(rxBleConnection.equals(RxBleConnection.RxBleConnectionState.CONNECTED)){
isConnected = true;
Log.d(TAG, "ble connect: " + isConnected);

} else if (rxBleConnection.equals(RxBleConnection.RxBleConnectionState.DISCONNECTED)) {
isConnected = false;
Log.d(TAG, "ble connect: " + isConnected);

}
return rxBleConnection.setupNotification(Constants.READ_CHAR_UUID);
})

flatMap을 사용해서 반환하는 Observable 객체를 Observable<RxBleConnection>에서 Observable<Observable<byte>>로 바꿔준다.

return rxBleConnection.setupNotification(Constants.READ_CHAR_UUID);

setupNotification은 ble장치가 notify하는 값을 받기위해 설정하는 메소드이다.

.doOnNext(byteObservable -> Log.d(TAG, "setUp Notification: " + byteObservable))
.flatMap(observable -> {
return observable;
})

doOnNext로 디버깅 한 번 해주고

Observable이 2번 씌워진 것을 1번 벗겨주기위해서 flatMap을 사용한다.

.observeOn(AndroidSchedulers.mainThread())

메인쓰레드에서 observable의 값에 대한 대응을 해줄거다

.subscribe(bytes -> {
Log.d(TAG, "Received Notification bytes " + byteArrayToHexaString(bytes));

Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_READ);
sendIntent.putExtra(Constants.SEND_MSG, new String(bytes));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
mContext.sendBroadcast(sendIntent);

}, error -> {
Log.d(TAG, "setUp Notification Error: " + error);
error.printStackTrace();
EventBus.getDefault().post(new DialogEvent(isConnected));
});

subscribe 메소드에서 ble 장치가 notify하는 byte 배열을 받아서 처리해준다.

connectDisposable.add(connectionDisposable);

CompositeDisposable에 add 해준다.

7. write하기

public void write(byte[] input) {
if(isConnected = true){
Disposable writeSubscription = Observable.just(connection)
        .firstOrError()
.flatMap(rxBleConnection -> {
return rxBleConnection.writeCharacteristic(WRITE_CHAR_UUID, input);
})
.observeOn(Schedulers.io())
.subscribe(bytes -> {
Log.d(TAG, "write: " + byteArrayToHexaString(bytes));

Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_WRITE);
sendIntent.putExtra(Constants.SEND_MSG, new String(bytes));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
mContext.sendBroadcast(sendIntent);
}, error -> {
Log.e(TAG, "write: ", error);
error.printStackTrace();
});

writeDisposable.add(writeSubscription);
}
}
Disposable writeSubscription = Observable.just(connection)
.firstOrError()

RxbleConnection을 Observable객체에 담은 다음에 firstOrError() 메소드로 Single 객체로 만들어준다.

firstOrError() 메소드는 Observable 객체가 비어있으면 에러를 발생시킨다.

.flatMap(rxBleConnection -> {
return rxBleConnection.writeCharacteristic(WRITE_CHAR_UUID, input);
})

flatMap 메소드에서 writeCharacteristic 메소드를 사용해서 write를 해준다.

input은 내가 ble 장치에 보낼 byte[] 이다.

input의 값은 고객사가 전달한 프로토콜 문서를 보고 작성하면 된다.

WRITE_CHAR_UUID는 ble 기기가 갖고있는 고유한 UUID 값이다.

.observeOn(Schedulers.io())
.subscribe(bytes -> {
Log.d(TAG, "write: " + byteArrayToHexaString(bytes));

Intent sendIntent = new Intent(Constants.SEND);
sendIntent.putExtra(Constants.SEND_FLAG, Constants.RECEIVER_WRITE);
sendIntent.putExtra(Constants.SEND_MSG, new String(bytes));
sendIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
mContext.sendBroadcast(sendIntent);
}, error -> {
Log.e(TAG, "write: ", error);
error.printStackTrace();
});

워커스레드에서 내가 보낸 바이트 배열을 처린한다.

writeDisposable.add(writeSubscription);

8. 연결끊기

public void disconnectBle(){
connectDisposable.clear();
isConnected = false;
Log.d(TAG, "ble disconnect");
connectionSource.onNext(isConnected);
}

간단하다.

connect(String macAddress) 와  subscribeConnection(RxBleDevice device)에서 구독한 disposable을 add한 CompositeDisposable을 clear() 해주면 된다.

Share:
Read More