Nouveau datalogger et graphiques sous Android

Nouveau datalogger et graphiques sous Android

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.

  YDataSet data = s.get_recordedData(0, 0);
  // 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.

public void loadFromYSensorDatalogger(YSensor sensor) throws YAPI_Exception {
    //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.

public synchronized void addMeasure(YMeasure measure) {
    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.

public synchronized int fillGraphSerie(XYSeries serie, double timestart, double timestop)
{
    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.

public class YoctopuceBgThread  implements Runnable, YAPI.DeviceArrivalCallback, YAPI.DeviceRemovalCallback, YSensor.TimedReportCallback {
        ...
    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
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.

Commenter aucun commentaire Retour au blog












Yoctopuce, get your stuff connected.