Truc et astuce pour notre libraire Android

Truc et astuce pour notre libraire Android

Nous avons souvent des questions de support sur l'utilisation de notre librairie Android. En effet, certains choix faits par Google obligent le développeur à faire très attention à la manière d’effectuer les opérations d'entrées/sorties, et plus particulièrement pour les accès réseau. Dans cet article, nous allons vous montrer notre solution à ce problème.




Les exemples Android qui sont inclus dans la documentation des modules Yoctopuce sont volontairement très basiques, le code contient le strict minimum pour pourvoir utiliser les modules Yoctopuce branchés par USB. Nous avons fait ce choix pour que la documentation reste lisible, mais si vous décidez d'écrire une application qui utilise un module qui est branché à YoctoHub, vous allez devoir faire face à quelques complications.

Nos modules peuvent être utilisés au travers d'une connexion USB mais aussi au travers d'une connexion réseau, par exemple s'ils sont branchés sur un YoctoHub. Dans tous les autres langages de programmation que nous supportons, il suffit de remplacer la chaîne de caractère "usb" par l'adresse IP du YoctoHub et le reste du code reste inchangé. Pour Android, il faut tenir compte d'une spécificité de l'OS: toutes les connexions réseau faites depuis le thread principal sont bloquées.

Ce choix à pour but de garantir que le thread principal, qui est responsable de l'interface, ne soit pas ralenti par les connexions réseau. A cause de cette limitation, la libraire Yoctopuce ne peut pas être utilisée depuis le thread principal. Cela force le développeur à écrire du code plus compliqué, qui doit gérer la communications entre plusieurs threads.

Pour bien comprendre les implications d'une telle architecture, imaginons une application qui doit afficher à l'écran la température d'un Yocto-Meteo quand un bouton est pressé. Pour que l’opération fonctionne sur un Yocto-Meteo branché sur un YoctHub, cette opération d'entrée/sortie doit être décomposée en trois étapes:

  1. Le clickHandler du bouton est exécuté depuis le thread principal
  2. Un deuxième thread lit la valeur du Yocto-Meteo et transmet la valeur au thread principal
  3. Le thread principal met à jour l'interface avec la valeur


Toutes les opérations qui utilisent la libraire Yoctopuce doivent être effectuées à l'étape 2, car les opérations réseau sont interdites sur le thread principal. L'étape 3 est nécessaire, car l'interface peut être modifiée uniquement depuis le thread principal.

Google propose plusieurs solutions qui permettent de faciliter l’écriture de ce genre de séquences. La plus simple et la plus répandue est l’utilisation d'AsyncTask.

Par exemple, la séquence précédente pourrait être écrite de la manière suivante:

public void onClick(View v)
{
    // creates an AsyncTask that reads a Yocto-Meteo value
    AsyncTask<Void, Void, Double> task = new AsyncTask<Void, Void, Double>(){
        @Override
        protected Double doInBackground(Void... params)
        {
            // part of the code that is executed in the second thread
            try {
                YTemperature ytemp = YTemperature.FindTemperature("");
                double value = ytemp.get_currentValue();
                return value;
            } catch (YAPI_Exception e) {
                e.printStackTrace();
                return null;
            }
        }

        @Override
        protected void onPostExecute(Double temp)
        {
            // updates display with the result on the main Thread
            if(temp!=null){
                _textView.setText(String.format("%f°",temp));
            }
        }
    };
    task.execute();
}



L'utilisation des AsyncTask permet de s'affranchir de l'allocation d'un nouveau thread et de la communication entre les deux threads. La méthode doInBackground est exécutée dans un second thread et le résultat est passé à la méthode onPostExecute qui est exécutée sur le thread principal. Le programmeur n'a pas à gérer le second thread.

Cette solution n'est cependant pas optimale, le code n'est pas très compact et il faut écrire une nouvelle AsyncTask pour chaque appel à la librairie Yoctopuce. De plus, toutes les AsyncTask de l'application sont sérialisées sur un même thread, ce qui peut poser des problèmes de performances.

Il existe plusieurs autres solutions, comme écrire son propre service ou gérer la communication avec un second thread manuellement, mais nous vous proposons une solution relativement simple et efficace qui permet d'utiliser notre librairie.

Nous avons écrit une classe YoctoActivity qui hérite de la classe Activity et qui s'occupe de démarrer un deuxième thread qui sera utilisé pour exécuter le code qui fait des accès réseau, en l’occurrence notre librairie Android. Cette classe peut donc être utilisée comme classe parente pour toutes les Activités de l'application qui utilisent un module Yoctopuce.

Cette classe définit deux méthodes qui permettent d’exécuter du code sur le second thread ou sur le thread principal. La méthode postBg() permet d’exécuter le code passé en argument sur le second thread, alors que la méthode postUI() permet d’exécuter du code sur le thread principal. Ces deux méthodes peuvent être appelées depuis n'importe quel thread sans aucune précaution particulière. Si on est déjà dans le bon thread, le code sera exécuté à la suite, si on est sur un autre thread, la classe YoctoActivity s'occupe de transférer l’exécution au bon thread.

En utilisant cette nouvelle super classe, il est possible de réécrire l'exemple précédent de la manière suivante:

public class MainActivity extends YoctoActivity {

  public void onClick(View v)
  {
    postBg(new BgRunnable() {
      @Override
      public void runBg() throws YAPI_Exception {
        YTemperature ytemp = YTemperature.FindTemperature("");
        final double temp = ytemp.get_currentValue();
        postUI(new Runnable() {
          @Override
          public void run() {
              _textView.setText(String.format("%f°",temp));

          }
        });
      }
    });
  }
}



Le code reste verbeux mais bien moins qu'avec une AsyncTask. C'est encore plus flagrant si l'on a pas besoin de mettre à jour l'interface. Par exemple, le code suivant enregistre le YoctoHub à l'adresse IP 192.168.1.14:

public void YoctopuceSetup()
{
    postBg(new BgRunnable() {
        @Override
        public void runBg() throws YAPI_Exception {
            YAPI.RegisterHub("192.168.1.14");
        }
    });
}



De plus, la classe YoctoActivity utilise son propre thread pour exécuter le code en arrière plan et n'est pas perturbée par d'autres AsyncTasks.

Cette solution à l'avantage de factoriser tout le code qui doit gérer la communication entre les threads dans une seule classe.

Voyons concrètement à quoi ressemble cette classe YoctoActivity:

public class YoctoActivity extends Activity {
  private Handler _uiHandler;
  private BGHandler _bgHandler;
  private HandlerThread _bgHandlerThread;


  @Override
  protected void onStart() {
    super.onStart();
    _bgHandlerThread = new HandlerThread("DetailHandlerThread");
    _bgHandlerThread.start();
    _uiHandler = new UIHandler(this);
    _bgHandler = new BGHandler(this, _bgHandlerThread.getLooper());
  }

  @Override
  protected void onStop() {
    _bgHandlerThread.quit();
    _bgHandlerThread.interrupt();
    super.onStop();
  }


  /**
   * Instances of static inner classes do not hold an implicit
   * reference to their outer class.
   */

  private static class UIHandler extends Handler {
    private final WeakReference<YoctoActivity> _weakReference;

    UIHandler(YoctoActivity activity) {
        _weakReference = new WeakReference<>(activity);
    }
  }


  /**
   * Instances of static inner classes do not hold an implicit
   * reference to their outer class.
   */

  static class BGHandler extends Handler {
    private final WeakReference<YoctoActivity> _hubActivityWeakReference;

    BGHandler(YoctoActivity service, Looper looper) {
        super(looper);
        _hubActivityWeakReference = new WeakReference<>(service);
    }


    void post(final BgRunnable bgRunnable) {
        post(new Runnable() {
            @Override
            public void run() {
                try {
                    bgRunnable.runBg();
                } catch (YAPI_Exception e) {
                    e.printStackTrace();
                    YoctoActivity activity = _hubActivityWeakReference.get();
                    if (activity != null) {
                        activity.handleBgIOError(e);
                    }
                }
            }
        });
    }
  }

  interface BgRunnable {
    void runBg() throws YAPI_Exception;
  }


  protected void postBg(BgRunnable bgRunnable) {
    _bgHandler.post(bgRunnable);
  }

  protected void postUI(Runnable fgRunnable) {
    _uiHandler.post(fgRunnable);
  }

  /**
   * This method is called from second thread you cannot access
   * UI form this method.
   *
   * @param e the YAPI_Exception thrown by the bg thread
   */

  private void handleBgIOError(YAPI_Exception e) {
    e.printStackTrace();
  }

}



En interne, la classe YoctoActivity utilise deux Handler qui permettent de sérialiser l’exécution de code sur le thread principal ou sur le second thread. Le fonctionnement des HandlerThread et Handler nécessiterait à lui seul un article, mais pour simplifier un HandlerThread est un Thread auquel on peut facilement poster des objets Runnable à l'aide d'un objet Handler.

Afin de ne pas utiliser des ressources inutilement, le thread est démarré dans la méthode onStart() et est stoppé dans la méthode onStop(), c'est-à-dire que le thread est utilisable uniquement lorsque l'activité est en premier plan. Pendant ce temps, le thread attend "intelligemment" du code à exécuter, c'est-à-dire qu'il ne va pas utiliser tout le temps CPU dans une attente active.

Si vous désirez utiliser cette solution, il n'est pas nécessaire de comprendre la totalité du code, il faut simplement ajouter cette classe à votre projet et suivre quelques règles:

  • Faire héritez vos activités de la classe YoctoActivity
  • Ne pas utiliser la méthode postBg() si l'activité n'est pas en premier plan


Vous avez maintenant une solution pour utiliser facilement un module derrière un YoctoHub dans une application Android.

Note: Quand le module Yoctopuce est connecté sur le port USB de la tablette, il est tentant de ne pas se préoccuper ce de problème et d'utiliser les modules Yoctopuce depuis le thread principal, cependant il est plus sage de d'appliquer les mêmes précautions. D'une part, cela permet de garder une interface réactive et des animations fluides. D'autre part, ça permet de garder l'application compatible avec les modules Yoctopuce accessibles par réseau, par exemple pour permettre aux utilisateurs d'utiliser des tablettes bon marché qui n'ont pas de support USB OTG.

Commenter aucun commentaire Retour au blog












Yoctopuce, get your stuff connected.