La semaine passée, nous avons utilisé la nouvelle version de la librairie pour tracer un graphique dynamique en C# .NET. A cette occasion, nous avons vu que la nouvelle API du Datalogger et les Timed Report facilitaient grandement le travail. Cette semaine nous allons réaliser une petite application Android qui va aussi tracer un graphique avec les valeurs du datalogger et les valeurs courantes. Mais nous allons voir que les choses sont un peu plus compliquées sous Android...
En théorie, écrire une application Android identique à notre application .NET devrait être trivial: Java et C# sont deux langages de programmation très proches et toutes nos librairies définissent les mêmes objets et méthodes. Du reste, l'utilisation de la classe du Datalogger et des Timed Report est identique: tout ce que nous avons vu à ce sujet la semaine passée est aussi valable pour Android (Il est donc recommandé d'avoir lu cet article)
Mais il y a une grosse différence dans la manière de gérer l'interface entre une application Android et une application .NET.
L'interface sous .NET
En .NET, le code qui utilise les modules Yoctopuce est exécuté dans le thread de l'interface. C'est-à-dire que l'application est soit en train de gérer l'interface (redessiner le graphique, etc.), soit en train de communiquer avec les modules Yoctopuce. C'est pour cette raison que nous avons dû ajouter un appel à Application.DoEvents() lors du chargement des données du datalogger. Sans cet appel, l'interface se figerait pendant le chargement des données.
// chargement de données
int progress = data.loadMore();
while (progress < 100)
{ progressBar.Value = progress;
Application.DoEvents();
progress = data.loadMore();
}
L'interface sous Android
Android a une approche différente, le thread de l'interface ne fait que le strict minimum : mettre à jour l'interface avec les bonnes informations. Toutes les opérations d'entrée-sortie (accès réseau, accès disque, accès USB) doivent être effectuées dans un autre thread. Cela garantit à l'utilisateur que l’interface restera réactive même si une opération d'entrée/sortie est très lente.
Il faut donc exécuter tout le code qui utilise les modules Yoctopuce dans un deuxième thread. Mais si on essaie de mettre à jour directement l'interface depuis ce deuxième thread, l'application va très rapidement avoir des problèmes. En effet, tôt ou tard, le thread de l'interface et votre thread vont essayer de modifier en même temps un même objet: dans le meilleur des cas des données vont être corrompues, dans le pire des cas l'application va planter.
La solution est d’implémenter un objet ThreadSafeSensorqui sert d'intermédiaire entre ces deux threads. L'objet ThreadSafeSensor garde une copie des informations nécessaires et garantit qu'un seul thread utilise ces informations à la fois.
L'objet ThreadSafeSensor garde une copie des informations du senseur Yoctopuce suivantes:
- le serial number
- le functionId
- le friendlyName
- l'unité
- un tableau de toutes les YMeasure
- un booléen pour savoir si on est en train de charger les données du datalogger
On définit des méthodes thread safe (avec le mot clef synchronized) pour accéder et modifier ces données. De cette manière, quand le second thread met à jour les mesures, on est sûr que le thread de l’interface n'est déjà en tain de les utiliser. Et quand le thread d'interface redessine le graphique il peut utiliser les dernières données sans exécuter d'opération d'entrée/sortie.
Les méthodes qui utilisent le thread d'entrée/sortie sont loadFromYSensorDatalogger() et addMeasure().
La méthode loadFromYSensorDatalogger() charge toutes les données du dataLogger et les met à jour.
//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();
}
}
La méthode addMeasure() ajoute une nouvelle valeur à notre liste de mesures. On utilise cette méthode pour mettre à jour le graphique avec les valeurs live une fois que toutes les données du datalogger sont chargées.
if (_measures == null) {
_measures = new ArrayList<YMeasure>();
}
_measures.add(measure);
double roundvalue = measure.get_averageValue() * _iresol;
_lastValue = Math.round(roundvalue) / _iresol;
}
Le thread d'interface utilise la méthode fillGraphSerie() pour mettre à jour les points du graphique en utilisant les données déjà chargées.
{
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;
}
Maintenant que nous avons notre objet ThreadSafeSensor, on peut regrouper les différentes parties de l'application de la semaine passée dans un seul thread YoctopuceBgThread. Comme pour notre application .NET, on implémente trois callbacks : DeviceArrivalCallback, DeviceRemovalCallbacky et TimedReportCallback.
Ces trois callbacks appellent les méthodes de notre objet ThreadSafeSensor.
...
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();
}
}
...
}
Et voilà l'application
Le code complet de l'application a été ajouté à la nouvelle librairie v1.10 disponible sur GitHub, et l'application est directement disponible sur Google Play.
Conclusion
La vraie difficulté de cette application est de gérer la communication entre les deux threads (le thread d'interface et le thread responsable de la communication avec les modules Yoctopuce). Ce n'est pas quelque chose qui est propre à Yoctopuce, si vous écrivez une application qui doit télécharger des informations sur Internet, vous aurez les mêmes problèmes. C'est cependant quelque chose qui est assez déroutant la première fois que l'on écrit une application Android et méritait donc ces quelques explications.