Last week, we used the new version of the library to trace a dynamic graph in C# .NET. At that time, we saw that the new Datalogger API and the TimedReport made the work a lot easier. This week, we are going to develop a small Android application that also traces a graph from the datalogger and from current values. But we are going to see that things are a little more complex under Android ...
Theoretically, writing an Android application identical to our .NET application should be obvious: Java and C# are two very close programming languages and all our libraries define the same objects and methods. Moreover, use of the Datalogger and TimedReport classes is identical: everything we saw on this topic last week remains valid for Android (You are therefore strongly advised to have read this article).
There is, however, an important difference in the manner to manage the interface between an Android application and a .NET application.
The interface under .NET
With .NET, the code using Yoctopuce modules is run in the interface thread. That is, the application is either managing the interface (redrawing the graph, etc.), or communicating with the Yoctopuce modules. This is why we had to add a call to Application.DoEvents() when loading the data from the data logger. Without this call, the interface would freeze when the data are loading.
// data loading
int progress = data.loadMore();
while (progress < 100)
{ progressBar.Value = progress;
Application.DoEvents();
progress = data.loadMore();
}
The interface under Android
Android takes a different approach. The interface thread does only the strict minimum: updating the interface with the correct information. All the input/output operations (network access, disk access, USB access) must be performed in another thread. This guarantees the user that the interface remains reactive even if an input/output operation is very slow.
We must therefore run the code using the Yoctopuce modules in a second thread. But if we try to update the interface from this second thread, we will hit in trouble. Indeed, sooner or later, the interface thread and the second thread are going to try to modify the same object at the same time. In the best case, some data will be corrupted. In the worst case, the application will crash.
The solution is to implement a ThreadSafeSensor object which serves as an intermediary between these two threads. The ThreadSafeSensor object keeps a copy of the necessary data and guaranties that only one thread uses this information at a time.
The ThreadSafeSensor object keeps a copy of the following Yoctopuce sensor information:
- serial number
- function ID
- friendlyName
- unit
- an array of all the YMeasure
- a boolean to know whether we are modifying the datalogger data
We define thread safe methods (with the synchronized keyword) to access and modify these data. In this way, when the second thread updates the measures, we are sure that the interface thread is not already using them. And when the interface thread redraws the graph, it can use the data without any input/output operation.
Methods using the input/output thread are loadFromYSensorDatalogger() and addMeasure().
The loadFromYSensorDatalogger() method loads all the data from the data logger and updates them.
//data loading
YDataSet data = sensor.get_recordedData(0, 0);
int progress = data.loadMore();
while (progress < 100){
progress = data.loadMore();
}
//transfer into an array
synchronized(this){
_measures = data.get_measures();
}
}
The addMeasure method adds a new value to our measure list. We use this method to update the graph with live values when all the data from the logger are loaded.
if (_measures == null) {
_measures = new ArrayList<YMeasure>();
}
_measures.add(measure);
double roundvalue = measure.get_averageValue() * _iresol;
_lastValue = Math.round(roundvalue) / _iresol;
}
The interface thread uses the fillGraphSerie() method to update the graph points using the already loaded data.
{
int count = 0;
if (_measures == null) {
return count;
}
for (YMeasure m : _measures) {
double end = m.get_endTimeUTC();
if (end >= timestart && end < timestop) {
// double x = m.get_endTimeUTC() * 1000;
double y = m.get_averageValue();
serie.add(end * 1000, y);
count++;
}
}
return count;
}
Now that we have our ThreadSafeSensor object, we can regroup the different parts from last week's application into a single YoctopuceBgThread thread. As for our .NET application, we implement three callbacks: DeviceArrivalCallback, DeviceRemovalCallbacky, and TimedReportCallback.
These three callbacks call methods of our ThreadSafeSensor object.
...
private final Context _appcontext;
// the code of the backgound thread
@Override
public void run()
{
//initializes the Yoctopuce API to use USB devices
// and registers arrival and removal callbacks
try {
YAPI.EnableUSBHost(_appcontext);
YAPI.RegisterDeviceArrivalCallback(this);
YAPI.RegisterDeviceRemovalCallback(this);
YAPI.RegisterHub("usb");
} catch (YAPI_Exception e) {
e.printStackTrace();
YAPI.FreeAPI();
return;
}
// main loop that will only trigger callback:
while(YoctopuceBgThread.stillRunInBG()) {
try {
YAPI.UpdateDeviceList();
YAPI.Sleep(1000);
} catch (YAPI_Exception e) {
e.printStackTrace();
}
}
YAPI.FreeAPI();
}
@Override
public void yDeviceArrival(YModule module)
{
try {
String serial = module.get_serialNumber();
YSensor ysensor = YSensor.FirstSensor();
while (ysensor != null) {
if (ysensor.get_module().get_serialNumber().equals(serial)) {
String functionId = ysensor.get_functionId();
ThreadSafeSensor sens = new ThreadSafeSensor(serial, functionId);
SensorStorage.get().add(sens);
_appcontext.sendBroadcast(new Intent(ACTION_SENSOR_LIST_CHANGED));
sens.updateFromYSensor(ysensor);
sens.loadFromYSensorDatalogger(ysensor);
ysensor.set_reportFrequency("60/m");
ysensor.set_logFrequency("60/m");
ysensor.registerTimedReportCallback(this);
sens.setLoading(false);
_appcontext.sendBroadcast(new Intent(ACTION_SENSOR_LIST_CHANGED));
}
ysensor = ysensor.nextSensor();
}
} catch (YAPI_Exception e) {
e.printStackTrace();
}
}
@Override
public void yDeviceRemoval(YModule module)
{
try {
String serial = module.get_serialNumber();
SensorStorage.get().removeAll(serial);
_appcontext.sendBroadcast(new Intent(ACTION_SENSOR_LIST_CHANGED));
} catch (YAPI_Exception e) {
e.printStackTrace();
}
}
public ArrayList<String> getHubs()
{
return _hubs;
}
@Override
public void timedReportCallback(YSensor sensor, YMeasure measure)
{
try {
String hwid = sensor.getHardwareId();
//Log.d(TAG,"New measure for" + hwid+":"+measure.get_averageValue());
ThreadSafeSensor graph = SensorStorage.get().get(hwid);
graph.addMeasure(measure);
if (System.currentTimeMillis()-_lastUpdate > 500){
Intent intent = new Intent(ACTION_SENSOR_NEW_VALUE);
intent.putExtra(EXTRA_HWID, hwid);
_appcontext.sendBroadcast(intent);
_lastUpdate = System.currentTimeMillis();
}
} catch (YAPI_Exception e) {
e.printStackTrace();
}
}
...
}
And here is the application
The full application code was added to the new v1.10 library available on GitHub. The application is directly available on Google Play.
Conclusion
The real difficulty of this application is to manage communications between the two threads (the interface thread and the thread in charge of communication with the Yoctopuce modules). This is not something specific to Yoctopuce. If you write an application which must download information from the Internet, you will encounter the same difficulties. It is however quite puzzling the first time you write an Android application and so we thought these explanations would be of use.