I'm writing an embedded system which doesn't have an internet connection, so the main interaction is using BLE from an Android device. (ESP32 is using the NimBLE-Arduino library)
I have some write characteristics and some read characteristics. Each one individually works well, but when I try to read immediately after write (or vice versa), only the first callback in the ESP32 is called
ESP32 code
// Setup function
void Bluetooth::setupCharacteristic()
{
NimBLEService *pService = m_pServer->createService(SERVICE_UUID);
NimBLECharacteristic *getStationsChr = pService->createCharacteristic(GET_STATIONS_CHR_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::READ_AUTHEN);
NimBLECharacteristic *setTimeChr = pService->createCharacteristic(SET_TIME_CHR_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::WRITE_AUTHEN);
getStationsChr->setCallbacks(this);
setTimeChr->setCallbacks(this);
pService->start();
}
// Read/Write callbacks
void Bluetooth::onRead(NimBLECharacteristic* pCharacteristic){
log_i("%s", pCharacteristic->getUUID().toString().c_str());
log_i(": onRead(), value: %s",pCharacteristic->getValue().c_str());
if (m_pCallback)
{
auto value = pCharacteristic->getValue();
//m_pCallback->onMessageReceived(&value);
}
...
}
void Bluetooth::onWrite(NimBLECharacteristic* pCharacteristic) {
std::string characteristicStr = pCharacteristic->getUUID().toString();
std::string value_str(pCharacteristic->getValue().c_str());
log_i("%s: onWrite(), value: %s (%s)", characteristicStr.c_str(), pCharacteristic->getValue().c_str(), value_str.c_str());
// Process incoming value and call the callback
// ...
if (m_pCallback)
{
auto value = pCharacteristic->getValue();
//m_pCallback->onMessageReceived(&value);
}
...
}
Android code
private fun updateTime() {
val calendar = GregorianCalendar()
val ts = Instant.now().epochSecond
val timeZone = timezonesMap?.get(calendar.timeZone.id) ?: ""
val timeMessage = TimeMessage(ts, 0, timeZone)
val setTimeChar = bluetoothGatt?.getService(SERVICE_UUID)?.getCharacteristic(SET_TIME_UUID)
if (setTimeChar?.isWritable() == true) {
val timeStr = gson.toJson(timeMessage)
Log.d("updateTime", "setting time to $timeStr")
sendToCharacteristic(setTimeChar, timeStr)
}
}
// This function is used to break long message to multiple messages, this is a simplified version
// for brevity
private fun sendToCharacteristic(characteristic: BluetoothGattCharacteristic?, value: String)
{
Log.d(TAG, "Sending to write characteristic ${characteristic?.uuid} value ($value)")
characteristic?.value = value.toByteArray()
bluetoothGatt?.writeCharacteristic(characteristic)
TimeUnit.MILLISECONDS.sleep(500)
}
private fun getStations() {
Log.d("getStations", "getting stations")
val getStationsChar = bluetoothGatt?.getService(SERVICE_UUID)?.getCharacteristic(GET_STATIONS_UUID)
if (getStationsChar?.isReadable() == true) {
Log.d("getStations", "reading")
bluetoothGatt?.readCharacteristic(getStationsChar)
}
}
The issue is when calling my sync() function
fun sync() {
Log.d("sync", "syncing")
bluetoothGatt?.apply {
if (device.bondState != BluetoothDevice.BOND_BONDED) {
device.createBond()
}
else {
updateTime()
// getStations()
}
}
}
If I uncomment the getStations, the read callback isn't called. If I comment the updateTime and uncomment the getStations, the read callback is called. switching the order doesn't make a difference. First called function works, second doesn't. Also adding a 1 second delay between the calls doesn't work
Your problem lies on the Android side.
In GATT you may only have one outstanding request. This means you must wait for the previous callback before executing a new operation. In particular, you must in this case wait for onCharacteristicWrite on your BluetoothGattCallback object after performing a write operation, before you can execute a next operation.
Adding a sleep doesn't work since you block the thread and hence prevent the result callback from running, whenever a response is received from the remote device.
Related
I am trying to build a BLE Gatt Server with multiple custom services and multiple characteristics.
To begin with I used the Google Example: https://github.com/androidthings/sample-bluetooth-le-gattserver/tree/master/kotlin
This was straight forward and worked very well. I modified the UUIDs to fit mine and I could receive notifications and write to the chars with no problem.
This is where I define the services and chars:
fun createTimeService(): BluetoothGattService {
val service = BluetoothGattService(TIME_SERVICE,
BluetoothGattService.SERVICE_TYPE_PRIMARY)
// Current Time characteristic
val currentTime = BluetoothGattCharacteristic(CURRENT_TIME,
//Read-only characteristic, supports notifications
BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ)
val configDescriptor = BluetoothGattDescriptor(CLIENT_CONFIG,
//Read/write descriptor
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE)
currentTime.addDescriptor(configDescriptor)
// Local Time Information characteristic
val localTime = BluetoothGattCharacteristic(LOCAL_TIME_INFO,
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE)
service.addCharacteristic(currentTime)
service.addCharacteristic(localTime)
return service
}
fun createSerialService(): BluetoothGattService {
val service = BluetoothGattService(serialPortServiceID,
BluetoothGattService.SERVICE_TYPE_PRIMARY)
val serialData = BluetoothGattCharacteristic(serialDataCharacteristicID,
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_WRITE)
service.addCharacteristic(serialData)
return service
}
And here I am applying them to my server:
private fun startServer() {
bluetoothGattServer = bluetoothManager.openGattServer(this, gattServerCallback)
bluetoothGattServer?.addService(TimeProfile.createTimeService())
?: Log.w(TAG, "Unable to create GATT server")
bluetoothGattServer?.addService(TimeProfile.createSerialService())
?: Log.w(TAG, "Unable to create GATT server")
// Initialize the local UI
updateLocalUi(System.currentTimeMillis())
}
I would expect that everything would be working like before after adding the second service. But now if I try to write/subscribe to any of the characteristics (doesn't matter in which service) I just receive this:
W/BluetoothGattServer: onCharacteristicWriteRequest() no char for handle 42
W/BluetoothGattServer: onDescriptorWriteRequest() no desc for handle 43
I found what was going wrong. Apparently you cannot just add all services at once like I did. Adding the second service before the first one was confirmed lead to an Exception setting the services to null.
In the end I solved this by adding only one service initially.
Then in the onServiceAdded() Callback of the BleGattServerCallback() I started one after another.
A function from some SDK is returning me a CompletableFuture. How can I read the value properly once it's reached.
My Code:
CompletableFuture<Web3ClientVersion> web3clientCompletableFuture;
web3clientCompletableFuture = web3jNode.web3ClientVersion().sendAsync();
sendAsync() Code (In SDK):
public CompletableFuture<T> sendAsync() {
return web3jService.sendAsync(this, responseType);
}
I can access the returned data using get(), but that would make the whole process syncronus and block UI.
I've checked the functions signatures on Android API Reference, like:
thenApply(Function<? super T, ? extends U> fn)
handle(BiFunction<? super T, Throwable, ? extends U> fn)
But seems I require some code examples.
[Note: I'm not so familiar with lambda]
Here is a tutorial that has examples that show you how to use these powerful methods of CompletableFuture. You are right you want to use thenApply() if you have to return value after you process the future. But if you simply want to process the future and not return anything, you should use thenAccept() and thenRun(). There are other methods listed with examples as well.
Here is an example that simply returns a CompletableFuture of type integer:
CompletableFuture<Integer> mynumber = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
mynumber = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return 4 * 4;
});
}
Here arg is the result(CompletableFuture) from the above step, and in your case the data you are receiving from the SDK. You are attaching a callback method(thenApply()) and do what ever you would like to do with it. Depending on your implementation, you can attach multiple thenApply(). Here I am calling a method that will take the result and do some computation with it.
CompletableFuture<Integer> willdoStuff = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
willdoStuff = mynumber.thenApply(arg -> {
compute(arg);
return arg / 2;
});
}
public void compute(int someInt){
Toast.makeText(getApplicationContext(), "The result of the computation is" + someInt, Toast.LENGTH_LONG).show();
}
Just comment out the sleep code to execute this code in the main thread.
The Lambda function is simply input and output, the arguments before {} being the input and the statement within the {}, which is actually a function that does something with the arguments(input). You might want to ask different question in regards to that.
I have an odd issue I can't explain the reason for it - maybe someone here can shed some light on it
I have a ticket scanning app in Xamarin Forms currently testing it on android
the interface allows you to:
type an order number and click the check order Button
use the camera scanner to scan which automatically triggers check order
use the barcode scanner to scan which automatically triggers check order
after the check order validation, user has to select the number of tickets from a drop down list and press confrim entry button
what I'm trying to do, is if the seats available on that ticket is just 1 - then automatically trigger confirm entry button functionality
problem that I have is that - some of my logic depends on setting the drop down index in code - for some reason it doesn't update - as seen in the debugger shot here
and this is the second tme I've noticed this today, earlier it was a var I was trying to assign a string and it kept coming up as null - eventually I replaced that code
is this a bug in xamarin ?
code has been simplified:
async void OnCheckOrderButtonClicked(object sender, EventArgs e)
{
await ValidateOrderEntry();
}
private async void scanCameraButton_Clicked(object sender, EventArgs e)
{
messageLabel.Text = string.Empty;
var options = new ZXing.Mobile.MobileBarcodeScanningOptions();
options.PossibleFormats = new List<ZXing.BarcodeFormat>() {
ZXing.BarcodeFormat.QR_CODE,ZXing.BarcodeFormat.EAN_8, ZXing.BarcodeFormat.EAN_13
};
var scanPage = new ZXingScannerPage(options);
scanPage.OnScanResult += (result) =>
{
//stop scan
scanPage.IsScanning = false;
Device.BeginInvokeOnMainThread(async () =>
{
//pop the page and get the result
await Navigation.PopAsync();
orderNoEntry.Text = result.Text;
//automatically trigger update
await ValidateOrderEntry();
});
};
await Navigation.PushAsync(scanPage);
}
private async Task ValidateOrderEntry()
{
//...other code....
checkInPicker.Items.Clear();
if (availablTickets == 1)
{
checkInPickerStack.IsVisible = true;
checkInPicker.SelectedIndex = 0;
messageLabel.Text = "Ticket OK! - " + orderNoEntry.Text;
messageLabel.TextColor = Color.Green;
//select the only element
checkInPicker.SelectedIndex = 0;
await PostDoorEntry();
}
//...other code....
}
private async Task PostDoorEntry()
{
int entryCount = checkInPicker.SelectedIndex + 1;
//... more code...
//...post api code..
}
Maybe I'm overlooking something, but you clear all the items a few lines above the one you are pointing out. That means there are no items in your Picker and thus you can't set the SelectedIndex to anything other than -1, simply because there are no items.
I use the odata4j library to access a WCF Data Service.
This is how I call a Service Method from my Android code:
OQueryRequest<OEntity> l = consumer.getEntities("GetDataList")
.custom("dataId", String.format("'%s'", actualData.ID))
.orderBy("Name").skip(0).top(200);
I checked it with WireShark, and I see that every method call is preceded with 2 calls of metadata information request:
Why? Are they essential? The metadata information is quite heavy, it shouldn't request is every time (not to mention 2 times).
What should I do to prevent odata4j from requesting metadata information so many times?
I found in the source code where the 'extra' request happens (in odata4j/odata4j-core/src/main/java/org/odata4j/consumer/AbstractODataConsumer.java ):
#Override
public EdmEntitySet findEdmEntitySet(String entitySetName) {
EdmEntitySet rt = super.findEdmEntitySet(entitySetName);
if (rt == null && delegate != EdmDataServices.EMPTY) {
refreshDelegate();
rt = super.findEdmEntitySet(entitySetName);
}
return rt;
}
It seems that if the entity set can't be found, the consumer creates an extra roundtrip to the server to get the metadata again (by calling refreshDelegate()):
private void refreshDelegate() {
ODataClientRequest request = ODataClientRequest.get(AbstractODataConsumer.this.getServiceRootUri() + "$metadata");
try {
delegate = AbstractODataConsumer.this.getClient().getMetadata(request);
} catch (ODataProducerException e) {
// to support services that do not expose metadata information
delegate = EdmDataServices.EMPTY;
}
}
I don't quite understand why: maybe it assumes that the server has changed and a new version of the metadata is available so it tries again.
If it fails then it tries to find a function with the given name.
Personally I don't consider this very effective unless the server side is so volatile that it changes between calls.
So, if you have no changing metadata on the server, it is safe to remove the check for the entitySet and let it return as a null:
#Override
public EdmEntitySet findEdmEntitySet(String entitySetName) {
EdmEntitySet rt = super.findEdmEntitySet(entitySetName);
//if (rt == null && delegate != EdmDataServices.EMPTY) {
// refreshDelegate();
// rt = super.findEdmEntitySet(entitySetName);
//}
return rt; //if it is null, then the search for a function will commence
}
I have to develop an Android application using phongap that retrieves the sensors data from the device.
One of the sensors I have to listen to is the ambient light sensor. This sensor has no implementation in phoneGap, so I have to add it as a plugin to PhoneGap.
I know how to add plugin and I know how to access the ALS data from Java - but in order to be sure that I am implementing it well I want to implement it as PhoneGap implements other sensors like Accelerometer. Therefor I wrote a ALSManager class in java that I implemented as I found the Accelerometer was implemented here:
https://github.com/apache/cordova-android/blob/master/framework/src/org/apache/cordova/AccelListener.java
and added lightSensor and lightValues modules like the acceleromter and acceleration modules.
But when I run this application I got following error message:
TypeError: Object # has no method 'getCurrentLight'
(and in the lightSensor module I have getCurrentLight method).
does any one can please suggest me what I am missing? or what do I have to do?
Thanks in advance,
The code I added in the cordova-2.5.0.js. Let me know if it's not enough:
// file: lib/common/plugin/LightValues.js
define("cordova/plugin/LightValues", function(require, exports, module) {
var Acceleration = function(lux, timestamp) {
this.lux = lux;
this.timestamp = timestamp || (new Date()).getTime();
};
module.exports = LightValues;
});
// file: lib/common/plugin/lightSensor.js
define("cordova/plugin/lightSensor", function(require, exports, module) {
/**
* This class provides access to device accelerometer data.
* #constructor
*/
var argscheck = require('cordova/argscheck'),
utils = require("cordova/utils"),
exec = require("cordova/exec"),
LightValues = require('cordova/plugin/LightValues');
// Is the accel sensor running?
var running = false;
// Keeps reference to watchAcceleration calls.
var timers = {};
// Array of listeners; used to keep track of when we should call start and stop.
var listeners = [];
// Last returned acceleration object from native
var light = null;
// Tells native to start.
function start() {
exec(function(a) {
var tempListeners = listeners.slice(0);
light = new LightValues(a.lux, a.timestamp);
for (var i = 0, l = tempListeners.length; i < l; i++) {
tempListeners[i].win(light);
}
}, function(e) {
var tempListeners = listeners.slice(0);
for (var i = 0, l = tempListeners.length; i < l; i++) {
tempListeners[i].fail(e);
}
}, "Light", "start", []);
running = true;
}
// Tells native to stop.
function stop() {
exec(null, null, "Light", "stop", []);
running = false;
}
// Adds a callback pair to the listeners array
function createCallbackPair(win, fail) {
return {win:win, fail:fail};
}
// Removes a win/fail listener pair from the listeners array
function removeListeners(l) {
var idx = listeners.indexOf(l);
if (idx > -1) {
listeners.splice(idx, 1);
if (listeners.length === 0) {
stop();
}
}
}
var lightSensor = {
/**
* Asynchronously acquires the current acceleration.
*
* #param {Function} successCallback The function to call when the acceleration data is available
* #param {Function} errorCallback The function to call when there is an error getting the acceleration data. (OPTIONAL)
* #param {AccelerationOptions} options The options for getting the accelerometer data such as timeout. (OPTIONAL)
*/
getCurrentLight: function(successCallback, errorCallback, options) {
//argscheck.checkArgs('fFO', 'lightSensor.getCurrentLight', arguments);
var p;
var win = function(a) {
removeListeners(p);
successCallback(a);
};
var fail = function(e) {
removeListeners(p);
errorCallback && errorCallback(e);
};
p = createCallbackPair(win, fail);
listeners.push(p);
if (!running) {
start();
}
},
/**
* Asynchronously acquires the acceleration repeatedly at a given interval.
*
* #param {Function} successCallback The function to call each time the acceleration data is available
* #param {Function} errorCallback The function to call when there is an error getting the acceleration data. (OPTIONAL)
* #param {AccelerationOptions} options The options for getting the accelerometer data such as timeout. (OPTIONAL)
* #return String The watch id that must be passed to #clearWatch to stop watching.
*/
watchLight: function(successCallback, errorCallback, options) {
//argscheck.checkArgs('fFO', 'lightSensor.watchLight', arguments);
// Default interval (10 sec)
var frequency = (options && options.frequency && typeof options.frequency == 'number') ? options.frequency : 10000;
// Keep reference to watch id, and report accel readings as often as defined in frequency
var id = utils.createUUID();
var p = createCallbackPair(function(){}, function(e) {
removeListeners(p);
errorCallback && errorCallback(e);
});
listeners.push(p);
timers[id] = {
timer:window.setInterval(function() {
if (light) {
successCallback(light);
}
}, frequency),
listeners:p
};
if (running) {
// If we're already running then immediately invoke the success callback
// but only if we have retrieved a value, sample code does not check for null ...
if (light) {
successCallback(light);
}
} else {
start();
}
return id;
},
/**
* Clears the specified accelerometer watch.
*
* #param {String} id The id of the watch returned from #watchAcceleration.
*/
clearWatch: function(id) {
// Stop javascript timer & remove from timer list
if (id && timers[id]) {
window.clearInterval(timers[id].timer);
removeListeners(timers[id].listeners);
delete timers[id];
}
}
};
module.exports = lightSensor;
});
I think maybe the problem is that you are adding your plugin code to cordova-2.5.0.js file. Instead what you should do is create a standalone JS file for each of your JavaScript files and then cordova.require() those files in the HTML page where you want to use that functionality.
So, create LightValues.js and LightSensor.js as separate files somewhere in your www folder. Then in your HTML file, make sure to include the JS file: <script type="text/javascript" src="path-to-lightSensor.JS-file"> (You'd only need to include this one file since it require()s the second one.)
Next, in deviceReady() function, you can call the light sensor with var lightSensor = cordova.require("cordova/plugin/lightSensor"). Notice that the cordova/plugin/lightSensor is not the path to the JS file but the name of it module that you declared in the define() section when you wrote the plugin.
After this you should be able to call lightSensor.getCurrentLight(). If you console.log(lightSensor) you would expect to see all of the available methods that you wrote.
Note that I'm not positive that cordova.require and cordova.define work in cordova-2.5. I'd hope that they do but this page sort of suggests it may not be supported until 2.6. If you are having problems after splitting the files out, maybe it is because of this.
I have just developed a light sensor plugin and fortunately succeed. I have just read the code above and found some small mistakes about identifier e.g "var Acceleration = function(lux, timestamp)" the variable should be LightValues instead of Acceleration. So please check the code first to void some essential mistakes. Then, I first use "lux" as the name of the variable but I got an undefined value of light when debugging the program. So I change "lux" into "x" and it did work!
There are 6 files should be paid attention to: the "LightListener.java", "LightValues.js", "LightSensor.js","cordova_plugin.js", "index.html" and "config.xml". If you configure all the 6 files, the program should work.