Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Mieux gérer les capteurs sous Windows 8

[new:15/01/2013]Dans un récent billet (Faites frémir les sens de Windows 8 ou comment taquiner les capteurs) je vous présentais les capteurs disponibles sous Windows 8 et la façon de les interroger, dans ce billet je vous propose de créer des classes helper pour gérer encore plus facilement les données issues de ces capteurs.

Les capteurs : des amis du développeur

Le meilleur moyen de conquérir le marché c’est de créer des apps qui sortent de l’ordinaire, et quel beau moyen d’innover que d’aller chercher des utilisations originales du côté des capteurs sensoriels !

Les capteurs sont les amis des développeurs astucieux et imaginatifs, ne l’oubliez pas !

Dans le dernier billet sur ce sujet un lecteur proposait d’utiliser un boitier ODB-II pour gérer les paramètres de sa voiture… Des softs existent pour cela, mais pas sur Surface. Encore un marché à prendre ! Il existe de nombreuses sondes externes fonctionnant en Bluetooth, des thermomètres, des détecteurs de rayons Gamma, des sondes de température pour la cuisine, des sondes médicales…

Mais si on peut penser à toutes les applications utilisant des capteurs externes que tout bon geek bricoleur adore en général, il y a déjà fort à faire en exploitant correctement la brochette de ceux présents “in the box” !

Du code pour gérer plus facilement les capteurs

Dans mon billet de présentation des capteurs je vous ai donné à chaque fois un exemple de code qui permet de lire les valeurs.

Toutefois on peut faire mieux que ces exemples bruts. On peut construire des classes spécialisées qui faciliteront grandement l’accès aux données des capteurs.

C’est ce code que je vais vous présenterai plus loin.

Sans répéter ce que j’ai déjà expliqué dans le billet précédent sur le sujet, avant de passer au code regardons rapidement le type d’information retourné par les API.

Allo ? Surface ? Me captez-vous ?

Notre amie Surface est dotée de certains capteurs et d’un OS exposant des API pour communiquer avec eux. Il est intéressant de savoir faire la nuance entre les capteurs réels, ceux qui sont virtuels et ceux que Surface RT ne supportent pas pour ne pas se fatiguer à inventer des idées qui ne pourront pas marcher sur ce matériel…

Surface RT (je ne sais pas encore si cela sera différent sur la Pro) propose les capteurs physiques suivants :

  • Capteur de lumière ambiante
  • Accéléromètre
  • Gyromètre
  • Magnétomètre

 

Il y a déjà de quoi s’amuser un peu, même si certains regrettent l’absence d’un GPS (que je ne juge pas très utile d’ailleurs pour un matériel étudié pour une utilisation principalement en intérieur, mais sa présence aurait été un plus). On pourrait ajouter à cette liste le Bluetooth et la Wifi car d’une certaine façon ce sont aussi des capteurs qui peuvent être utilisés pour obtenir des informations (après tout la géolocalisation des API WinRT se sert aussi de la Wifi pour connaitre la position dans certains cas). De même on pourrait ajouter les caméras (arrière et frontale) qui peuvent être détournées de leur but premier pour servir de capteurs sophistiqués. Toutefois je me limiterai ici aux capteurs “standard”.

Grâce à cette série de capteurs physiques, Surface offre via WinRT plusieurs API qui interprètent les données brutes pour fournir des informations exploitables :

  • L’accéléromètre
  • Le gyromètre
  • Le compas (grâce au magnétomètre)
  • Le capteur de lumière
  • Le capteur d’orientation
  • Un capteur d’orientation simplifié
  • Un inclinomètre

 

C’est tout de suite plus riche vu comme ça…

Le lecteur pourra trouver sur le site MSDN des informations techniques précises notamment sur l’exploitation des capteurs de mouvement et de de position (Integrating Motion and Orientation Sensors). Le site contient bien entendu bien d’autres pages dédiées à la programmation des capteurs qui sont au moins aussi intéressantes.

Surface et le monde physique

Dès lors qu’on souhaite tirer avantage (et information) des capteurs il est essentiel de poser une convention immuable : un ensemble d’axes alignés avec la tablette qui permet de savoir où est la gauche de la droite, le haut du bas, le devant du derrière.

Bien entendu, à l’œil, cela saute aux …yeux. Mais pour ce qui est des capteurs, du processeur et de l’OS, rien ne saute aux yeux, il faut une convention.

Les axes utilisés par les API sont ainsi définis comme le montre le dessin suivant :

L’axe des Y monte le long du côté gauche de la tablette, celui des X part du même point pour suivre le bord inférieur horizontal de la tablette, et l’axe des Z, partant toujours du même point, s’étend dans la direction du regard au-delà de la tablette. La position 0,0,0 se trouvant donc en bas à gauche. Les valeurs négatives sont autorisés bien entendu.

Des axes et des unités

Chaque capteurs, en tout cas cas chaque API de capteur réel ou simulé, retourne des données qui seront toujours relatives au repère cartésien à 3 dimensions tel que montré sur le dessin ci-dessus (sauf pour la lumière ou le compas).

De plus, ces informations sont retournées dans des systèmes d’unité propres à chaque capteur.

L’accéléromètre

Il mesure l’accélération. Et même au repos il ne donnera jamais une lecture à zéro sur les 3 axes à la fois, sauf dans l’espace lointain (loin de toute planète ou étoile et à condition d’être animé d’un mouvement rectiligne uniforme, donc non accéléré). En effet n’oubliez pas que sur notre planète même en train de dormir sur une plage ensoleillée, même à siroter un pastis dans une chaise longue, vous êtes en train de tomber ! En tout cas c’est ce qui fait vous ne volez pas au-dessus de votre serviette de bain quand vous êtes allongé sur la plage… Notre planète créée un champ gravitationnel permanent dont la valeur moyenne mesurée en G est de 1.

Donc même au repos posée sur votre bureau Surface ne pourra jamais retourner trois valeurs à zéro pour la mesure de l’accéléromètre.

Quoi qu’on pourrait justement construire un jeu dont le but serait d’envoyer la tablette en l’air de telle façon à ce qu’un moment son accélération sur les trois axes, notamment la verticale, soit à zéro… Cela réclamerait pas mal d’entrainement (car il ne faudrait aucune déviation sur les axes X et Y). C’est finalement ce que propose le CNES et “Air Zero G” désormais avec des vols paraboliques ouverts au public pour environ 6000 euros… Là c’est un peu pareil, si vous échouez à rattraper la tablette, hop! c’est 600 à 1000 euros qui partent en fumée SourireQui aura le cran de créer ce jeu ? (en cas de succès je réclame ma part de royalties pour l’idée, mais en cas de casse je décline toute responsabilité !).

Donc l’accéléromètre mesure l’accélération sur les 3 X de façon indépendante et il retourne une valeur Double exprimée en G. 

Le Gyromètre

Comme je le précisais dans le billet précédent sur ce sujet, il ne faut pas confondre gyromètre et gyroscope, les technologies sont différentes, même si quelque part on peut en tirer le même type d’information, c’est à dire une accélération angulaire.

C’est d’ailleurs ce qui diffère de l’accéléromètre, car tous les deux mesures une accélération. Mais le gyromètre s’intéresse à l’accélération angulaire donc autour des 3 axes (rotations).

Le gyromètre retourne une valeur exprimée en degrés par seconde, c’est un Double aussi, et ce, pour les 3 axes.

Le compas

Cette API utilise le magnétomètre embarqué. Par force, ce dernier s’aligne selon le champ magnétique local et pointe donc théoriquement le nord magnétique (sauf perturbations locale du champ magnétique). Toutefois l’API sait retourner le nord géographique.

La donnée retournée est une mesure d’angle en degrés (un Double) qui indique la déviation par rapport au nord, soit géographique soit magnétique.

Il faut noter qu’il n’y a pas de calcul savant pour donner le nord géographique d’après le nord magnétique, on ne peut que calculer une approximation mais avec le danger de se tromper fortement si le champ magnétique local est perturbé (aimant, bloc d’alimentation, masses conductrices…). De fait, la mesure du nord géographique qui se doit d’avoir une certaine fiabilité (sinon on ne peut absolument pas s’y fier et cela rend la mesure sans intérêt pratique) utilise généralement les signaux GPS plutôt que le magnétomètre.

Surface n’étant pas équipée de GPS, la valeur du nord géographique n’est pas calculable avec une fiabilité suffisante et il semble que Microsoft a préféré ne rien retourner dans ce cas. Donc avec Surface, le compas ne peut être utilisé que pour lire le nord magnétique, ce qui n’a finalement que peut d’intérêt, du moins en tant qu’instrument de navigation.

On peut en revanche se servir du magnétomètre pour détecter des champs magnétiques… Reste à trouver des idées d’applications qui font un usage décisif de cette information…

Capteur de lumière

Surface se sert de ce capteur pour régler automatiquement la luminosité de l’écran. Il ne retourne pas une information très riche, juste un nombre de Lux (ce coup-ci c’est un Float).

Toutefois il ne faut pas minimiser ce capteur qui peut être utilisé par exemple pour basculer automatiquement l’affichage dans un mode “nuit”, ce que peuvent exploiter des logiciels de cartographie céleste par exemple (passage de tout l’affichage en nuances de rouges sur fond noir ce qui évite l’éblouissement quand on regarde un ciel nocturne où l’œil finit par être en mydriase).

On peut donc, j’en suis certain, trouver milles et une utilisations astucieuses même si cela ne peut être en soi le cœur d’une application (mais après tout une idée géniale n’est pas interdite !).

Le capteur d’orientation

Cette API retourne une information complexe sous la forme d’un Quaternion et d’une matrice de rotation. Sont exploitation directe est un peu délicate et oblige à entrer dans des calculs spécifiques à la 3D. Les applications de jeu, ou par exemple de pilotage via Wifi d’un hélicoptère radiocommandé peuvent tirer un grand profit de ces données. Mais cela limite tout de même leur champ d’application.

Les lecteurs intéressés par les calculs dans l’espace avec des Quaternions peuvent lire ce papier “Quaternions and their applications to rotation in 3D space” (PDF d’une quinzaine de pages environ).

Le capteur d’orientation simplifiée

Cette API rend l’information précédente plus facilement utilisable dans une application standard (par exemple pour choisir une vue selon l’orientation de la tablette). Elle ne retourne qu’une seule valeur, issue d’une énumération, qui indique la position simplifiée de la tablette.

Simplifiée car il n’y a plus de nuance au degré près, juste une indication “grossière” sur la position : NotRotated, Rotated90DegreesCounterClockwise, Rotated180DegreesCounterClockwise, Rotated270DegreesCounterClockwise, Faceup, Facedown.

Un bon moyen de savoir si la tablette est posée face à l’endroit ou non sur la table par exemple. Une information qui peut être exploitée astucieusement.

Par exemple sur le Samsung SIII lorsque le téléphone est posé à plat à l’envers (écran vers la table) cela coupe le son des alertes… ultra pratique quand on ne veut pas être dérangé et sans aller bricoler des paramètres qu’il faut ensuite rétablir.

L’inclinomètre

Cette API retourne trois informations qui se réfèrent aux trois degrés de liberté en indiquant la rotation de la machine sur les 3 axes.

L’information est retournée en degrés (Double).

On parle ici de Pitch, Roll et Yaw, qui sont des termes d’aviation, ce que le petit dessin ci-dessous résume (il faut imaginer une tablette à la place de l’avion !) :

 

Le Pitch s’appelle en français le Tangage, le Roll se traduit par Roulis et le Yaw par Lacet.

L’information retournée se base en réalité sur plusieurs capteurs physiques et il semble d’ailleurs que même le compas soit mis à contribution puisque si la tablette est parfaitement alignée sur le nord magnétique on peut arriver à une lecture de zéro sur les trois axes.

Du Code !

Les explications c’est chouette, mais le code c’est bien aussi…

Comme annoncé en début d’article je vais vous proposer de regarder quelques classes que j’ai développées pour tenter d’unifier les capteurs. Car il faut noter que hélas WinRT ne propose pas quelque chose de très unifié et que les API des capteurs ne descendent pas par exemple d’une classe commune, ce qui en faciliterait l’exploitation. Les classes que j’ai écrites simplifient cet accès .

Ce code est simple et peut largement être adapté pour vos propres applications, en l’état il constitue une bonne base pour vos propres réalisations.

Organisation du code

La partie réutilisable, la bibliothèque de code, se trouve dans l’espace de noms “Enaxos.W8.Sensors”. Les classes principales reprennent exactement le même nom que celles de WinRT. C’est un choix délibéré : d’abord si on les utilise c’est pour masquer les classes de WinRT, et ensuite puisque .NET gère depuis toujours des espaces de noms, il est très facile de pointer vers mes classes ou celles de WinRT en les préfixant correctement.

Du point vue de la logique générale j’ai tenté d’unifier ce qui ne l’était pas et ce n’est pas facile à faire puisque, comme je le mentionnais plus haut, les API WinRT des différents capteurs ne descendent même pas d’une classe ou d’une interface commune qui pourrait être un “levier” dans cette unification.

Tout en haut de la hiérarchie se trouve ISensor, une interface classique qui est définie comme suit :

 public interface ISensor
    {
        bool Initialized { get; }
        uint MinimumReportInterval { get; }
        uint ReportInterval { get; set; }
        DateTime LastEvent { get; }
        void Open();
        void Close();
        event PropertyChangedEventHandler PropertyChanged;
        string ToString();
    }

Cette interface regroupe tout ce que j’ai pu trouver de commun ou que j’ai pu unifier.

Initialized est un indicateur booléen qui permet de savoir si l’initialisation d’un capteur s’est déroulée correctement suite à un appel à Open(). Par défaut une instance créée n’est pas liée à son capteur, elle peut donc subsister en mémoire et servir ponctuellement sans que cela ne pose de problème, notamment en utilisant Close() qui libère en quelque sorte le capteur.

Il n’y a pas de libération à proprement parlé (de type Dispose par exemple) car les capteurs se présentent comme des Singletons qu’on peut utiliser directement. Les classes que j’ai écrites se lient à ce singleton sur Open() et s’en détache sur Close(), c’est à dire que les évènements sont supprimés et que la référence à la device est passée à null.

Tous les capteurs, sauf un ou deux, propose de régler l’intervalle d’interrogation (ReportInterval) et retournent la valeur minimale acceptable pour cet intervalle (MinimumReportInterval). C’est pourquoi ces deux propriétés ont pu être fédérées.

Le PropertyChanged et le ToString() font partie de l’interface car on doit pouvoir les manipuler.

LastEvent est une donnée commune à tous les capteurs, elle retourne le stamp de la dernière lecture des données. C’est cette propriété que vous traquerez dans un gestionnaire de PropertyChanged, car lorsque cet évènement est déclenché, vous avez l’assurance que toutes les données utiles ont été lues depuis le capteur. Inutile donc de chercher à connaitre le changement de chacune des propriétés.

En revanche, si vous effectuez des bindings directs sur les propriétés d’un capteur cela fonctionnera aussi puisque le PropertyChanged de chacune est bien déclenché aussi. On notera qu’ici aussi le code s’assure que toutes les données sont lues avant de déclencher les PropertyChanged au lieu de le faire donnée par donnée ce qui pourrait rendre assez complexe une lecture “groupée” des valeurs. Donc pour un capteur retournant 3 valeurs (X,Y,Z) par exemple, qu’on traque le PropertyChanged de X, de Y ou de Z, on peut à chaque fois être sûr que si traque X, la lecture de Y et Z est valide, et que si on traque Y la lecture de X et Z sont valides aussi, etc.

En réalité la programmation choisie permet d’avoir l’avantage des deux mondes : un monde dans lequel chaque valeur peut être traquée et lue indépendamment, et un monde dans lequel toutes les valeurs seraient lues en un seul bloc (ce qui est en réalité le fonctionnement des API WinRT).

 

A partir de ISensor, le code classique doit tirer sa révérence pour laisser la place à du code générique. C’est puissant le code générique mais cela ne permet pas d’avoir un ancêtre vraiment commun… D’où la présence de l’interface ISensor.

La classe Sensor qui servira de base à chaque capteur est ainsi une classe abstraite et générique. Son code supporte ISensor :

public abstract class Sensor<T> : INotifyPropertyChanged, ISensor where T : class
    {
        #region fields
 
        private bool initialized;
        // ReSharper disable InconsistentNaming
        protected DateTime lastEvent = DateTime.MinValue;
        // ReSharper restore InconsistentNaming
        protected T device;
        protected const double Epsilon = 1e-7;
 
        #endregion
 
        #region Properties
 
        /// <summary>
        /// Gets access to the WinRT device API
        /// </summary>
        public T Device
        {
            get { return initialized ? device : null; }
            protected set
            {
                if (initialized) return;
                if (device == value) return;
                device = value;
                OnPropertyChanged();
            }
        }
 
        /// <summary>
        /// Returns true if the device has been correctly initialized
        /// </summary>
        public bool Initialized
        {
            get { return initialized && Device != null; }
            protected set
            {
                if (value == initialized) return;
                initialized = value;
                OnPropertyChanged();
            }
        }
 
        /// <summary>
        /// Minimum report interval supported by the sensor
        /// </summary>
        public uint MinimumReportInterval
        {
            get { return GetMinimumReportInterval(); }
        }
 
        /// <summary>
        /// Get/Set sensor report interval
        /// </summary>
        public uint ReportInterval
        {
            get { return GetReportInterval(); }
            set
            {
                if (value < MinimumReportInterval) value = MinimumReportInterval;
                var v = GetReportInterval();
                SetReportInterval(value);
                if (v != value) OnPropertyChanged();
            }
        }
 
        /// <summary>
        /// Last event date and time
        /// </summary>
        public DateTime LastEvent
        {
            get { return lastEvent; }
            protected set
            {
                if (!initialized) return;
                if (lastEvent == value) return;
                lastEvent = value;
                OnPropertyChanged();
            }
        }
 
        #endregion
 
        #region public methods and event
 
        /// <summary>
        /// Opens the sensor
        /// </summary>
        public void Open()
        {
            if (initialized) return;
            DoOpen();
            if (device == null) return;
            ReportInterval = MinimumReportInterval;
            Initialized = true;
        }
 
        /// <summary>
        /// Closes the sensor
        /// </summary>
        public void Close()
        {
            if (!initialized) return;
            DoClose();
            Initialized = false;
        }
 
 
        /// <summary>
        /// Property changed
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
 
        #endregion
 
        #region protected / virtual 
 
        /// <summary>
        /// internal property changed trigger
        /// </summary>
        /// <param name="propertyName">property name (optional)</param>
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
 
        /// <summary>
        /// Opens the device. must be overriden by real implementation
        /// </summary>
        protected virtual void DoOpen()
        {
            throw new NotImplementedException();
        }
 
        /// <summary>
        /// Closes the device. must be overriden by real implementation
        /// </summary>
        protected virtual void DoClose()
        {
            throw new NotImplementedException();
        }
 
        /// <summary>
        /// Returns minimum report interval. must be overriden by real implementation
        /// </summary>
        /// <returns></returns>
        protected virtual uint GetMinimumReportInterval()
        {
            throw new NotImplementedException();
        }
 
        /// <summary>
        /// Returns report interval. must be overriden by real implementation
        /// </summary>
        /// <returns></returns>
        protected virtual uint GetReportInterval()
        {
            throw new NotImplementedException();
        }
 
        /// <summary>
        /// Sets minimum report interval. must be overriden by real implementation
        /// </summary>
        /// <param name="value"></param>
        protected virtual void SetReportInterval(uint value)
        {
            throw new NotImplementedException();
        }
 
        public override string ToString()
        {
            return GetType().Name;
        }
 
        #endregion
 
    }

 

Une fois posée les bases de ISensor et de la classe abstraite Sensor, on peut créer des classes spécialisées pour chaque capteur.

Je ne vais pas lister ici le code chaque capteur, cela serait fastidieux d’autant que vous retrouverez le code source en fin de billet… Prenons juste un exemple très simple, le capteur de lumière :

 public class LightSensor : Sensor<Windows.Devices.Sensors.LightSensor>
    {
 
        #region fields
 
        private float lux;
 
        #endregion
 
        #region Property
 
        public float Lux
        {
            get { return lux; }
            set
            {
                if (Math.Abs(value - lux) < Epsilon) return;
                lux = value;
                OnPropertyChanged();
            }
        }
 
        #endregion
 
 
 

void deviceReadingChanged(Windows.Devices.Sensors.LightSensor sender,

Windows.Devices.Sensors.LightSensorReadingChangedEventArgs args)

        {
            lux = args.Reading.IlluminanceInLux;
            lastEvent = args.Reading.Timestamp.DateTime;
            // no matter which event is used, all values are set before the handler is called.
            // ReSharper disable ExplicitCallerInfoArgument
            OnPropertyChanged("Lux");
            OnPropertyChanged("LastEvent");
            // ReSharper restore ExplicitCallerInfoArgument        
        }
 
        protected override void DoOpen()
        {
            device = Windows.Devices.Sensors.LightSensor.GetDefault();
            if (device == null) return;
            device.ReadingChanged += deviceReadingChanged;
            // ReSharper disable ExplicitCallerInfoArgument
            OnPropertyChanged("Device");
            // ReSharper restore ExplicitCallerInfoArgument
        }
 
        protected override void DoClose()
        {
            if (device == null) return;
            device.ReadingChanged -= deviceReadingChanged;
            device = null;
            // ReSharper disable ExplicitCallerInfoArgument
            OnPropertyChanged("Device");
            // ReSharper restore ExplicitCallerInfoArgument
        }
 
        protected override uint GetMinimumReportInterval()
        {
            return device != null ? device.MinimumReportInterval : 0;
        }
 
        protected override uint GetReportInterval()
        {
            return device != null ? device.ReportInterval : 0;
        }
 
        protected override void SetReportInterval(uint value)
        {
            if (device == null) return;
            device.ReportInterval = value;
        }
 
        public override string ToString()
        {
            return base.ToString() + " - " + string.Format("Lux: {0:0.0000} ", lux)
                + lastEvent.ToString();
        }
    }

 

Ce capteur ne retourne qu’une seule donnée : le nombre de Lux perçus par la sonde de lumière ambiante.

Toutefois elle montre le fonctionnement global d’une classe spécialisée créée à partie de Sensor, notamment la surcharge de toutes les méthodes virtuelles et l’exposition des valeurs du capteur.

L’application de test en marche

image

L’application de test est très simple : elle propose des checkbox pour chaque capteur et une listbox affichant toutes les lectures. Cet affichage exploite le ToString() personnalisé de chaque classe. Plusieurs capteurs peuvent être ouverts en même temps, mais cela devient très vite brouillon !

Pour aider aux test, il y a un bouton Clear List qui efface la liste. On libère en capteur en décochant sa checkbox.

On notera que les captures d’écran sont des photos faites avec mon Galaxy SIII et non des captures depuis le simulateur : Ce dernier ne permet pas de simuler de valeurs pour les capteurs et les API retournent “null” systématiquement. Il faut donc utiliser le mode debug distant avec une vraie Surface (très bluffant d’ailleurs, et dont j’ai parlé dans ce billet Debug d’applications WinRT sur Surface)

image

Ahh travailler la nuit dans un bureau qui clignote de partout de petites leds, illuminé par les écrans et autres zinzins électroniques… Un pur bonheur du geek Sourire

Conclusion

Je vous laisse sur cette rêverie de cabine de pilotage de navette spéciale (pas encore spatiale hélas)…

Sans oublier le code complet avec son projet de test :

blog comments powered by Disqus