I'm using RxAndroidBle 2 to watch a characteristic for CSC but I cannot read some characteristics before. It's also the first time I'm using ReactiveX so I have to deal with flatMap, flatMapSingle, etc.
Here's my code to register a handler for watching CSC Measure:
connectionSubscription = device.establishConnection(false)
// Register for notifications on CSC Measure
.flatMap(rxBleConnection -> rxBleConnection.setupNotification(Constants.CSC_MEASURE))
.doOnNext(notificationObservable -> {
// Notification has been set up
Log.d(TAG, "doOnNext = " + notificationObservable.toString());
})
.flatMap(notificationObservable -> {
Log.d(TAG, "flatMap = " + notificationObservable);
return notificationObservable;
}) // <-- Notification has been set up, now observe value changes.
.subscribe(
this::onCharacteristicChanged,
throwable -> {
if (throwable instanceof BleDisconnectedException) {
Log.e(TAG, "getCanonicalName = " + throwable.getClass().getCanonicalName());
}
}
);
And then the code for extracting the value:
private void onCharacteristicChanged(byte[] bytes) {
Integer f = ValueInterpreter.getIntValue(bytes, ValueInterpreter.FORMAT_UINT8, 0);
Log.d(TAG, "flags " + f);
switch (f) {
case 0: // 0x00000000 is not authorized
Log.w(TAG, "flags cannot be properly detected for this CSC device");
break;
case 1:
// sensor is in speed mode (mounted on the rear wheel)
// Cumulative Wheel Revolutions
Integer sumWheelRevs = ValueInterpreter.getIntValue(bytes, ValueInterpreter.FORMAT_UINT32, 1);
// Last Wheel event time
Integer lastWheelEvent = ValueInterpreter.getIntValue(bytes, ValueInterpreter.FORMAT_UINT16, 5);
Log.d(TAG, "Last wheel event detected at " + lastWheelEvent + ", wheel revs = " + sumWheelRevs);
break;
case 2:
// sensor is in cadence mode (mounted on the crank)
// Last Crank Event Time
// Cumulative Crank Revolutions
break;
}
}
I have another piece of working code to read ONE characteristic, but how can I process a list of characteristics?
.flatMapSingle(rxBleConnection -> rxBleConnection.readCharacteristic(Constants.DEVICE_HARDWARE))
.subscribe(
characteristicValue -> {
String deviceHardware = ValueInterpreter.getStringValue(characteristicValue, 0);
Log.d(TAG, "characteristicValue deviceHardware = " + deviceHardware);
},
throwable -> {
// Handle an error here.
}
)
Constants are defined like this:
public static final UUID CSC_MEASURE = UUID.fromString("00002a5b-0000-1000-8000-00805f9b34fb");
I tried to integrate the answer provided in here but the code doesn't compile anymore. Moreover, the code should combine up to 12 characteristics (simple map of UUID to Int/String/Boolean). I used to have a working code by creating a subclass of BluetoothGattCallback but my code was getting more and more difficult to maintain with standard Android Bluetooth classes.
I tried to integrate the answer provided in here but the code doesn't compile anymore.
I have updated the post you mentioned to match RxAndroidBle based on RxJava2. It should compile now.
I have another piece of working code to read ONE characteristic, but how can I process a list of characteristics?
...
Moreover, the code should combine up to 12 characteristics (simple map of UUID to Int/String/Boolean).
The mentioned post does solve a case where there are 4 characteristics. In case of 12 (or a variable number) there is a Single#zipArray function.
Single.zipArray(
/* Object[] */ results -> YourWrapperObject(results),
rxBleConnection.readCharacteristic(Constants.DEVICE_HARDWARE),
rxBleConnection.readCharacteristic(Constants.DEVICE_HARDWARE1),
rxBleConnection.readCharacteristic(Constants.DEVICE_HARDWARE2),
// ...
rxBleConnection.readCharacteristic(Constants.DEVICE_HARDWARE11)
)
Related
I badly need this to proceed further in my application.
I'm very much familiar with Android BLE and using for years.
I have the below code to enable notification and it is working for years with my peripheral. onCharacteristicChanged() method is called with "OK_N1" when notification is enabled.
private void enableNotification(String serviceUUID, String characteristicUUID) {
if (bluetoothGatt == null) {
return;
}
BluetoothGattService service = bluetoothGatt.getService(UUID.fromString(serviceUUID));
if (service == null) {
return;
}
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(characteristicUUID));
bluetoothGatt.setCharacteristicNotification(characteristic, true);
enableDescriptor(characteristic);
}
private void enableDescriptor(BluetoothGattCharacteristic bluetoothGattCharacteristic) {
if (bluetoothGatt == null) {
return;
}
BluetoothGattDescriptor descriptor = bluetoothGattCharacteristic.getDescriptor(
UUID.fromString(PodsServiceCharacteristics.CLIENT_CHARACTERISTIC_CONFIG));
if (descriptor == null)
return;
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
}
Now, I am using Polidea RxAndroidble(ver 1.7.0) with RxJava2 to make things easy.
I have the following code with Polidea's RxAndroidBle which is not working.
public void enableNotifications(#NotNull String[] characteristics) {
if (isConnected()) {
mNotificationSubscriber = mRxBleConnection.setupNotification(UUID.fromString(characteristics[0]))
.doOnNext(notificationObservable -> notificationHasBeenSetUp())
.flatMap(notificationObservable -> notificationObservable)
.subscribe(this::onNotificationReceived, this::onNotificationSetupFailure);
}
}
private void onNotificationReceived(byte[] bytes) {
Log.i(TAG, "onNotificationReceived");
}
private void onNotificationSetupFailure(Throwable throwable) {
Log.i(TAG, "onNotificationSetupFailure" + throwable.getMessage());
}
private void notificationHasBeenSetUp() {
Log.i(TAG, "notificationHasBeenSetUp");
}
notificationHasBeenSetUp() is called but onNotificationReceived() is not called, where I get "OK_N1" bytes
It is probably because your peripheral sends a notification right after the Client Characteristic Configuration Descriptor is set.
By default RxAndroidBle sets up notifications fully before emitting the Observable<byte[]> i.e. the CCC descriptor has to be successfully written before the emission.
Since library version 1.8.0 it is possible to use NotificationSetupMode.QUICK_SETUP which emits Observable<byte[]> as soon as possible — before the CCC descriptor is written allowing to capture such early notifications/indications.
mRxBleConnection.setupNotification(bluetoothGattCharacteristic, NotificationSetupMode.QUICK_SETUP)
Pre 1.8.0 version answer
To not miss any "early" emissions one may leverage NotificationSetupMode.COMPAT in which the library does not handle writing the CCC descriptor*.
(...)
mNotificationSubscriber = mRxBleConnection.discoverServices()
.flatMap(rxBleDeviceServices -> rxBleDeviceServices.getCharacteristic(characteristicUuid))
.flatMapObservable(bluetoothGattCharacteristic -> {
BluetoothGattDescriptor cccDescriptor = bluetoothGattCharacteristic.getDescriptor(PodsServiceCharacteristics.CLIENT_CHARACTERISTIC_CONFIG);
Completable enableNotificationCompletable = mRxBleConnection.writeDescriptor(cccDescriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
Completable disableNotificationCompletable = mRxBleConnection.writeDescriptor(cccDescriptor, BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE).onErrorComplete();
return mRxBleConnection.setupNotification(bluetoothGattCharacteristic, NotificationSetupMode.COMPAT)
.doOnNext(notificationObservable -> notificationHasBeenSetUp())
.flatMap(notificationObservable -> notificationObservable)
.mergeWith(enableNotificationCompletable)
.doOnDispose(disableNotificationCompletable::subscribe) // fire and forget
.share(); // this can be omitted but I put it here in case the resulting `Observable<byte[]>` would be shared among multiple subscribers
})
.subscribe(this::onNotificationReceived, this::onNotificationSetupFailure);
(...)
I think since this is apparently quite common situation it will be handled by the library at some point.
* NotificationSetupMode.COMPAT was introduced mainly for BLE peripherals that do not follow BLE specification and do not have CCC descriptor yet will send notifications all the time
My current code is as shown below. I am wondering how do i unsubscribe (disconnect the ble) after writeCharacteristic? Also, is there a way to reconnect on writeCharacteristic fail?
Subscription subscription = device.establishConnection(false)
.timeout(5000, TimeUnit.MILLISECONDS)
.flatMap(rxBleConnection ->
rxBleConnection.writeCharacteristic(fromString("00005551-0000-1000-8000-008055555fb"), hexToBytes(mData)))
.take(1).retry(2)
.subscribe(
characteristicValue -> {
Log.e(TAG, "write success " + characteristicValue + " " + device.getMacAddress());
// subscribeList.remove(key).unsubscribe();
},
throwable -> {
Log.e(TAG, "write error " + throwable);
// subscribeList.remove(key).unsubscribe();
}
);
I am wondering how do i unsubscribe (disconnect the ble) after connection?
I assume that after connection is after the write because if the connection ended by external factors (like the peripheral being turned off) then the whole flow would end with an error.
If you want to write only a single value and then disconnect then everything you need is to take a single value from the flow after writeCharacteristic() by using .take(1) before the .subscribe().
Also, is there a way to reconnect on writeCharacteristic fail?
First of all, a failure of rxBleConnection.writeCharacteristic() does not automatically close the connection since 1.3.0 version of RxAndroidBle if the error is related to the write itself.
You may be experiencing the connection being closed only because you do not handle errors of the write action. You can make the write to retry two times by using .retry(2) operator.
Be mindful which Observable you try to .retry(). If you are interested in retrying writes only if they fail but the connection is still valid then you should apply .retry() on the RxBleConnection.writeCharacteristic(). On the other hand—if you want to retry the whole flow if any error will occur then you should put the .retry() on the whole flow Observable.
Subscription subscription = device.establishConnection(false)
.flatMap(rxBleConnection ->
rxBleConnection.writeCharacteristic(
fromString("00005551-0000-1000-8000-008055555fb"),
hexToBytes(mData)
)
.retry(2) // retry here will only retry the write if a write characteristic will fail but the connection will still be intact
)
.take(1) // this will unsubscribe the above part of the Observable on the first valid emission from it—so after the write will complete
// .retry(2) if you will put retry here then for whatever reason the above flow [connecting + writing] will fail then a new connection will be established and write performed
.subscribe(
characteristicValue -> {
Log.e(TAG, "write success " + characteristicValue + " " + device.getMacAddress());
},
throwable -> {
Log.e(TAG, "write error " + throwable);
}
);
Just:
subscription.unsubscribe();
If I understand the process correctly, unsubscribing will "bubble up" through the operator stack to the top, thereby disconnecting.
I want to implement the Online/Offline/IsTying functionality using the pubnub library in android. For this i created the Pubnub connection.
//create the pubnub connection
PNConfiguration configuration = new PNConfiguration();
configuration.setPublishKey(PUBNUB_PUBLISH_KEY);
configuration.setSubscribeKey(PUBNUB_SUBSCRIBE_KEY);
configuration.setLogVerbosity(PNLogVerbosity.BODY);
configuration.setConnectTimeout(100000);
configuration.setSubscribeTimeout(31000);
configuration.setHeartbeatNotificationOptions(PNHeartbeatNotificationOptions.ALL);
configuration.setPresenceTimeoutWithCustomInterval(120,59);
configuration.setPresenceTimeout(120);
mPubNub = new PubNub(configuration);
And after that i set the subscriber callback to the listener
**strong text**
private SubscribeCallback subscribeCallback = new SubscribeCallback() {
#Override
public void status(PubNub pubnub, PNStatus status) {
Log.d("Chat", "status() called with: pubnub = [" + pubnub + "], status = [" + status + "]");
if (status.getOperation() != null) {
switch (status.getOperation()) {
// let's combine unsubscribe and subscribe handling for ease of use
case PNSubscribeOperation:
case PNUnsubscribeOperation:
Toast.makeText(ChatAppService.this, "Status : " + status.getOperation().name(), Toast.LENGTH_SHORT).show();
// note: subscribe statuses never have traditional
// errors, they just have categories to represent the
// different issues or successes that occur as part of subscribe
switch (status.getCategory()) {
case PNConnectedCategory:
// this is expected for a subscribe, this means there is no error or issue whatsoever
Toast.makeText(ChatAppService.this, "Status : " + status.getCategory(), Toast.LENGTH_SHORT).show();
break;
case PNReconnectedCategory:
// this usually occurs if subscribe temporarily fails but reconnects. This means
// there was an error but there is no longer any issue
Toast.makeText(ChatAppService.this, "Status : " + status.getCategory(), Toast.LENGTH_SHORT).show();
HashMap<String,String> map = new HashMap();
map.put("State","Online");
pubnub.setPresenceState().channels(Arrays.asList(Constants.GLOBAL_CHANNEL)).state(map).uuid(pubnub.getConfiguration().getUuid());
break;
case PNDisconnectedCategory:
// this is the expected category for an unsubscribe. This means there
// was no error in unsubscribing from everything
Toast.makeText(ChatAppService.this, "Status : " + status.getCategory(), Toast.LENGTH_SHORT).show();
HashMap<String,String> mapOffline = new HashMap();
mapOffline.put("State","Offline");
pubnub.setPresenceState().channels(Arrays.asList(Constants.GLOBAL_CHANNEL)).state(mapOffline).uuid(pubnub.getConfiguration().getUuid());
break;
case PNTimeoutCategory:
HashMap<String,String> mapTimeout = new HashMap();
mapTimeout.put("State","Offline");
pubnub.setPresenceState().channels(Arrays.asList(Constants.GLOBAL_CHANNEL)).state(mapTimeout).uuid(pubnub.getConfiguration().getUuid());
pubnub.reconnect();
break;
case PNUnexpectedDisconnectCategory:
// this is usually an issue with the internet connection, this is an error, handle appropriately
Toast.makeText(ChatAppService.this, "Status : " + status.getCategory(), Toast.LENGTH_SHORT).show();
pubnub.reconnect();
break;
case PNAccessDeniedCategory:
// this means that PAM does allow this client to subscribe to this
// channel and channel group configuration. This is another explicit error
Toast.makeText(ChatAppService.this, "Status : " + status.getCategory(), Toast.LENGTH_SHORT).show();
break;
default:
// More errors can be directly specified by creating explicit cases for other
// error categories of `PNStatusCategory` such as `PNTimeoutCategory` or `PNMalformedFilterExpressionCategory` or `PNDecryptionErrorCategory`
}
break;
case PNHeartbeatOperation:
// heartbeat operations can in fact have errors, so it is important to check first for an error.
// For more information on how to configure heartbeat notifications through the status
// PNObjectEventListener callback, consult <link to the PNCONFIGURATION heartbeart config>
if (status.isError()) {
// There was an error with the heartbeat operation, handle here
} else {
// heartbeat operation was successful
}
break;
default: {
// Encountered unknown status type
}
}
} else {
// After a reconnection see status.getCategory()
}
}
#Override
public void message(PubNub pubnub, PNMessageResult message) {
//TODO: If App open: Save to database and broadcast for database change else show Notification
}
#Override
public void presence(PubNub pubnub, PNPresenceEventResult presence) {
Log.d("chat", "presence() called with: pubnub = [" + pubnub + "], presence = [" + presence + "]");
}
};`
Finally the issue is that when i leave the group then no presence callback is coming.I should get the event of leave the channel with user state so that i can remove the user from the online user's list.
for unsubscribing the group
pubnub.unsubscribe().channels(Arrays.asList(channel)).execute()
when i subscribe the channel then presence callback calls three time
1. event : join
2. event : state-changes
3. event : leave
I am not getting how to handle this scenario in Android. Please help me i am stucking in this problem from last three days.
I'm wondering if anybody can help me figure out what is causing the data I am sending to become corrupt.
My setup is currently an Arduino pro mini with a HM-10 bluetooth module connected (I have also tried HM-11 Module too) and an Android application to receive the bluetooth data.
Module setup: http://letsmakerobots.com/node/38009
If I send data with big enough intervals then the data is fine, but if I send the data continuously I see messages getting mixed up and lost. To test this I send "$0.1,0.2,0.3,0.4,0.5" to the Android application from the Arduino, sometimes the stream of data appears to send fine but other times it is really quite scrambled. Please see the below graphs that demonstrate this:
Good case:
Bad case:
Arduino code:
String inputString = ""; //Hold the incoming data.
boolean stringComplete = false; //Determines if the string is complete.
boolean realtime = false;
void setup()
{
Serial.begin(9600);
delay(500);
Serial.print("AT+START");
delay(500);
}
void loop()
{
if(stringComplete)
{
if(inputString.equals("rStart"))
{
Serial.println("$startACK");
realtime = true;
}
else if(inputString.equals("stop"))
{
Serial.println("$stopACK");
realtime = false;
}
else{
Serial.print(inputString);
}
inputString = "";
stringComplete = false;
}
if(realtime)
{
Serial.println("$0.1,0.2,0.3,0.4,0.5,0.6");
delay(10);
}
}
void serialEvent() {
while (Serial.available())
{
// get the new byte:
char inChar = (char)Serial.read();
if (inChar == '\n')
{
stringComplete = true;
}
else
{
inputString += inChar;
}
}
}
The Android side just receives the data and then parses it in an IntentService:
#Override
protected void onHandleIntent(Intent intent) {
//Incoming command.
String rawData = intent.getStringExtra(DataProcessingIntentService.REQUEST);
//Append our new data to our data helper.
Log.i(this.getClass().getName(), "Previous Raw: (" + DataProcessingHelper.getInstance().getData() + ")");
DataProcessingHelper.getInstance().appendData(rawData);
Log.i(this.getClass().getName(), "New Raw: (" + DataProcessingHelper.getInstance().getData() + ")");
commandStartIndex = DataProcessingHelper.getInstance().getData().indexOf("$");
commandEndIndex = DataProcessingHelper.getInstance().getData().indexOf("\n");
//Set this as the data starting point.
if(commandStartIndex != -1){
DataProcessingHelper.getInstance().offsetData(commandStartIndex);
}
//Ensure that a command has been found and that the end index is after the starting index.
if(commandStartIndex != -1 && commandEndIndex > commandStartIndex){
//Remove the command structure from the command.
command = DataProcessingHelper.getInstance().getData().substring(commandStartIndex+1, commandEndIndex-1); //Remove the \r\n end command.
DataProcessingHelper.getInstance().offsetData(commandEndIndex+1);
if(command.length() > 1){
//Split the data out of the comand.
splitData = command.split(",");
Log.i(this.getClass().getName(), "Broadcasting the processed data. (" + command + ")");
//Broadcast data.
Intent broadcastIntent = new Intent();
broadcastIntent.setAction(DataProcessingIntentService.RESPONSE);
broadcastIntent.addCategory(Intent.CATEGORY_DEFAULT);
broadcastIntent.putExtra(DataProcessingIntentService.RESPONSE, splitData);
sendBroadcast(broadcastIntent);
}else{
Log.e(this.getClass().getName(), "Command is less than 1 character long!");
}
}
}
Thank you for any help!
I have now figured out what was causing this problem. It appears that BLE only supports a maximum of 20 bytes per a transaction. The time between these transactions is different depending on what you are using. I'm currently using notifications which means that I can send 20 bytes every 7.5 milliseconds maximum. I have opted for 10 milliseconds to be safe. I will now need to look into breaking up packets into 20 bytes maximum to ensure no data corruption.
I'm porting a game to use Google Play Game Services with multiplayer support. I'm using RealTimeSocket instead of realtime message because the game already has socket support.
To get the socket I call GamesClient.getRealTimeSocketForParticipant, and then I could get input and output streams as use it as a usual socket.
My problem is that if a device receives data before the call to getRealTimeSocketForParticipant, I will not be able to read this data. For instance:
Device A calls getRealTimeSocketForParticipant.
Device A sends "Hello".
Device B calls getRealTimeSocketForParticipant.
Device B receives nothing.
Device A sends "World".
Device B receives "World".
I have modified one of the example projects (ButtonClicker) and replicated the problem here. I have modified the code to use realtime socket, and modified the startGame method to this:
String mReceivedData = "";
byte mNextByteToSend = 0;
void startGame(boolean multiplayer)
{
mMultiplayer = multiplayer;
updateScoreDisplay();
switchToScreen(R.id.screen_game);
findViewById(R.id.button_click_me).setVisibility(View.VISIBLE);
GamesClient client = getGamesClient();
String myid = mActiveRoom.getParticipantId(client.getCurrentPlayerId());
ArrayList<String> ids = mActiveRoom.getParticipantIds();
String remoteId = null;
for(int i=0; i<ids.size(); i++)
{
String test = ids.get(i);
if( !test.equals(myid) )
{
remoteId = test;
break;
}
}
//One of devices should sleep in 5 seconds before start
if( myid.compareTo(remoteId) > 0 )
{
try
{
//The device that sleeps will loose the first bytes.
Log.d(TAG, "Sleeping in 5 seconds...");
Thread.sleep(5*1000);
}
catch(Exception e)
{
}
}
else
{
Log.d(TAG, "No sleep, getting socket now.");
}
try
{
final RealTimeSocket rts = client.getRealTimeSocketForParticipant(mRoomId, remoteId);
final InputStream inStream = rts.getInputStream();
final OutputStream outStream = rts.getOutputStream();
final TextView textView =((TextView) findViewById(R.id.score0));
//Thread.sleep(5*1000); Having a sleep here instead minimizes the risk to get the problem.
final Handler h = new Handler();
h.postDelayed(new Runnable()
{
#Override
public void run()
{
try
{
int byteToRead = inStream.available();
for(int i=0; i<byteToRead; i++)
{
mReceivedData += " " + inStream.read();
}
if( byteToRead > 0 )
{
Log.d(TAG, "Received data: " + mReceivedData);
textView.setText(mReceivedData);
}
Log.d(TAG, "Sending: " + mNextByteToSend);
outStream.write(mNextByteToSend);
mNextByteToSend++;
h.postDelayed(this, 1000);
}
catch(Exception e)
{
}
}
}, 1000);
}
catch(Exception e)
{
Log.e(TAG, "Some error: " + e.getMessage(), e);
}
}
The code ensures that one of the two devices sleeps 5 seconds before the call to getRealTimeSocketForParticipant. For the device that doesn't sleep the output will be something like:
No sleep, getting socket now.
Sending: 0
Sending: 1
Sending: 2
Sending: 3
Sending: 4
Received data: 0
Sending: 5
Received data: 0 1
Sending: 6
Received data: 0 1 2
That's expected, no data lost. But for the other device I get this:
Sleeping in 5 seconds...
Received data: 4
Sending: 0
Received data: 4 5
Sending: 1
Received data: 4 5 6
Sending: 2
Received data: 4 5 6 7
Sending: 3
The first bytes are lost. Is there anyway to avoid this?
If i'm understanding the API correctly, the messages exchanged through a real time socket are unrealiable, so you can't always have assurance that all players received all messages you sent. I couldn't find info about the network protocol used by RealTimeSocket, but I suspect it's UDP.
If that's really the case, I'm afraid there's little you can do short of implementing some sort of handshake yourself. Choose one device (ex.: the one with the lowest ID) to be the "synchronizer", and have it create a set with every other device. Send a message ("SYN") such as "where are you? x y z" (not literally, of course) every second, until the others respond "I'm here! (y)" ("ACK"). Remove from the set the devices that sent a response, until the set is empty. At this point, send everyone a "game's starting!" and go on.
Note that any of these messages can be lost: if the "ACK" is lost, next time the "SYN" is sent the device should answer again. If the "game's starting" message is lost, tough luck, the device will keep waiting until it receives a different message, at such point it should consider itself free to start (though delayed).
One last note: even if the underlying protocol is TCP, it's still not 100% reliable, no protocol is. See this question for more info, if you don't know this fact already.