IoT et interface naturelle (2) : réalité augmentée

Dans mon précédent article, j'ai présenté un type d'interface naturelle basée sur l'assistant vocal de Google.

Cette fois, nous allons explorer les possibilités de la réalité augmentée! Avant de commencer, une petite démonstration :

Comme vous pouvez le constater, le but est de récupérer en temps réel les données émises par un objet connecté (la poubelle), pour les afficher en réalité augmentée lors de son survol, avec n'importe quel type de terminal (smartphone, tablette...).

AR.js

L'interface en réalité augmentée est réalisée à partir du framework AR.js. Son intérêt principal est de s'appuyer sur les technos web (HTML, Javascript) et donc d'être utilisable sur toutes les plateformes. Voici un exemple de page web utilisée dans la démonstration précédente :


&html*
 &head*
  &title*ARjs&/title*
  &script src="https://aframe.io/releases/0.8.0/aframe.min.js"*&/script*
  &script src="https://rawgit.com/donmccurdy/aframe-extras/master/dist/aframe-extras.loaders.min.js"*&/script*
  &script src="https://jeromeetienne.github.io/AR.js/aframe/build/aframe-ar.js"*&/script*
  &script*
   /*AFRAME.registerComponent('cursor-listener', {
     init: function () {
    this.el.addEventListener('click', function (evt) {
      // alert('click');
    });
     }
   });
   // https://stackoverflow.com/questions/47032056/gltf-cursor-listener-click-event-in-a-frame
   AFRAME.registerComponent('raycaster-autorefresh', {
     init: function () {
    var el = this.el;
    this.el.addEventListener('model-loaded', function () {
      var cursorEl = el.querySelector('[raycaster]');
      cursorEl.components.raycaster.refreshObjects();
    });
     }
   });*/
  &/script*
 &/head*

 &body style='margin : 0px; overflow: hidden;'*
  &a-scene arjs="debugUIEnabled: false;"*
   &a-assets*
    &img id="full" src="img/full.png"/*
    &img id="empty" src="img/empty.png"/*
    &img id="medium" src="img/medium.png"/*
    &img id="low" src="img/low.png"/*
    &img id="recycle" src="img/recycle.png"/*
   &/a-assets*
   &!-- handle marker with hiro preset --*
   &!--&a-marker preset="hiro"*--*
   &a-marker-camera preset="custom" type='pattern' url='data/can-marker.patt'*
    &!--&a-image id="levelIndicator" src="#empty"  width="3" height="2" position="-2 2 0"*&/a-image*--*
    &a-text value="Fill level" position="0 2.6 0" width="10" anchor="left" color="#00BFFF"*&/a-text*
    &a-text test-counter font="exo2bold" id="fillLevel" value="0 %" position="0 2.1 0" width="15" anchor="left" color="#00BFFF"*&/a-text*
    &a-image src="#recycle"  width="1" height="1" position="-1.5 -2 0"*&/a-image*
    &a-text value="Next interv." position="-0.9 -1.6 0" width="10" anchor="left" color="#00BFFF"*&/a-text*
    &a-text id="date" font="exo2bold" value="01 Jan 2018" position="-0.9 -2.4 0" width="20" anchor="left" color="#00BFFF"*&/a-text*
    &a-entity position="-1.5 2 0"*
     &a-entity id="threeDcan" gltf-model="url(obj/trash.gltf);" position="0 0 0" cursor-listener*&/a-entity*
     &a-animation attribute="rotation"
        dur="4000"
        fill="forwards"
        to="0 360 0"
        repeat="indefinite"*&/a-animation*
     &a-animation begin="click" attribute="position" to="-1 2 0"
      easing="linear" dur="50" fill="backwards" repeat="10"*&/a-animation*
    &/a-entity*
   &/a-marker-camera*
   &!--&a-camera*
    &a-cursor*&/a-cursor*
   &/a-camera*--*
   &a-entity light="type: ambient; color: #FFF; intensity: 5;"*&/a-entity*
  &/a-scene*
  &script*
   ...
   document.querySelector('a-scene').querySelector('#date').setAttribute('value', getDate(daysCount));
   ...
  &/script*
 &/body*
&/html*

PS : pour des raisons de compatibilité, j'ai du remplacer dans cet extrait les '<' par '&' et '>' par '*'.

En détails

Quelques précisions sur le fonctionnement de cette démonstration :

  • Utilisation d'un tag personnalisé : le tag basique Hiro (utilisé par défaut par AR.js pour reconnaître la scène) a été remplacé par un tag personnalisé, via la balise : a-marker-camera preset="custom" type='pattern' url='data/can-marker.patt' et l'outil en ligne Marker Training
  • Intégration de modèle 3D animé : pour permettre l'intégration dans la scène d'un modèle 3D animé, il est nécessaire de se procurer un modèle (au format GLTF, via Sketchfab par ex) et d'intégrer le script aframe-extras.loaders.min.js
  • Gestion de l'éclairage : dès lors que vous ajoutez des modèles dans une scène, il faut commencer à jouer avec les lumières. Ici une lumière d'ambiance général a été mise en place : a-entity light="type: ambient; color: #FFF; intensity: 5;"
  • Manipulation du DOM : c'est ce qui constitue un des points fort du framework AR.js. Vous pouvez utiliser Javascript pour manipuler les éléments de la scène 3D de la même manière que le DOM 2D standard HTML!

Connectivité

Qui dit technos web, dit Javascript : il est donc possible d'utiliser n'importe quel code (par ex websockets) pour vous connecter à la source de données (la plateforme IoT) afin de récupérer toutes les données et créer des interactions avec la réalité augmentée!

Enjoy!


Fichier(s) joint(s) :



IoT et interface naturelle

Créer des applications technologiques c'est bien, mais les rendre accessibles de manière naturelle aux utilisateurs, c'est mieux!

Voici donc un exemple de comment mettre en place un cas d'usage complet : un chatbot ("robot conversationnel" !) vocal capable de récupérer les données émises par des objets.

chatbot iot cumulocity ai

Scénario fonctionnel

  • Un utilisateur pose une question à l'assistant vocal Google
  • Une fois interprétée, la version textuelle est transmise à un chatbot
  • Le chatbot comprend l'action à déclencher à partir de mots-clés
  • Une requête est envoyée vers la plateforme IoT pour récupérer les données
  • Le chatbot répond à l'utilisateur de manière naturelle en incluant une visualisation pertinente des données

Mise en place

Concrètement, ce genre de solution peut être mis en place à l'aide d'outils simples enchainés les uns aux autres. Pour la partie vocale, l'assistant de Google pour smartphone fait très bien le travail de reconnaissance. Le chatbot ainsi que son intelligence artificielle apportés par le service Recast.ai permettent d'apporter une fluidité très appréciée dans l'interaction homme-machine. Son atout principal repose sur le fait qu'il permet d'étendre un robot pré-entrainé, par exemple à faire la conversation, pour lui ajouter des compétences spécifiques à notre besoin. Cela garanti que même lorsque votre robot ne sera pas en mesure de reconnaitre une action correspondant à votre scénario, il répondra de manière naturelle en mode conversation.

L'autre grand intérêt de la solution Recast.ai est qu'elle est très orientée vers les développeurs, et fourni donc plusieurs types d'interfaces pour activer son robot selon les besoins : via des "BotConnector" vers les applications de messageries les plus répandues (Facebook, Slack, Tweeter, Skype...) mais également via une interface REST. Et la customisation du robot passe par l'écriture de code Javascript/NodeJS, hébergé par Recast.ai eux-mêmes.

Pour démarrer rapidement la mise en place de votre bot, vous pouvez suivre cette documentation, expliquant de A à Z toutes les étapes.

Retour d'expérience

Pour vous faciliter la tâche, voici quelques astuces afin d'optimiser l'élaboration de votre bot. Cet exemple s'appui sur les extraits de code issus de la documentation citée précédemment. Le code du bot ressemble donc à cela :


const recastai = require('recastai').default;
const client = new recastai(process.env.REQUEST_TOKEN);
const request = require('request');
const axios = require('axios');

export const bot = (body, response, callback) => {
    console.log(body);

// response, the response object from the server in case of a local run
// callback, the object called in case of a bot hosting run

    if (body.message) {
        // pour gérer les appels par BotConnector (Slack...)
        client.connect.handleMessage({body}, response, replyMessage);
    } else if (body.text) {
        // pour gérer les appels par API REST en direct
        replyMessage(null, body.text, callback);
    } else {
        callback('Requete vide?!');
    }
};

function replyMessage(message, textMessage, callback) {
    if (message) {
        console.log("handling BotConnector message");
    } else {
        console.log("handling API message");
    }
    const recastaiReq = new recastai.request(process.env.REQUEST_TOKEN, process.env.LANGUAGE);
    const contentMessage = message ? message.content : textMessage;
    recastaiReq.analyseText(contentMessage)
            .then(recastaiRes => {
    var varcontent = "";
                // get the intent detected
                var intent = recastaiRes.intent();
                if (intent) {
                    console.log("intent:" + intent.slug + "/" + intent.confidence);
                    if (intent.slug === 'c8y_geoloc' && intent.confidence > 0.7) {
                        if (recastaiRes.get('asset-type') && recastaiRes.get('number')) {
                            // type d'objet recherché (par ex 'caisse')
                            var asset = recastaiRes.get('asset-type').raw;
                            // id de l'objet
                            var number = recastaiRes.get('number').raw;

                            axios.get('',
                                    {
                                        headers: {"Authorization": "Basic ..."} 
                                    })
                                    .then(response => {
                                        var body = response.data;

                                        if (body.managedObject) {
                                            //... do stuff
                                            return message ? message.reply([{type: 'text', content: varcontent}]).then() :
                                                   callback(null, {result: varcontent, intent: intent.slug, data: dataResp});
                                        } else {
                                            varcontent = 'Je n\'ai rien trouvé!';
                                            return message ? message.reply([{type: 'text', content: varcontent}]).then() :
                                                    callback(null, {result: varcontent, intent: intent.slug});
                                        }
                                    })
                                    .catch(error => {
                                        varcontent = 'Il y a eu un problème...';
                                        return message ? message.reply([{type: 'text', content: varcontent + error}]) :
                                                callback(error, null);
                                    });
                        } else {
                            varcontent = 'Je ne sais pas quoi chercher...';
                            return message ? message.reply([{type: 'text', content: varcontent}]).then() :
                                    callback(null, {result: varcontent, intent: intent.slug});
                        }
                    } else {
                        // on fait appel au moteur de conversation, pour conserver l'intelligence par defaut du bot
                        const converseReq = new recastai.request(process.env.REQUEST_TOKEN, process.env.LANGUAGE);

                        return converseReq.converseText(contentMessage)
                                .then(function (res2) {
                                    // ...extract the reply...
                                    varcontent = res2.reply();

                                    return message ? message.reply([{type: 'text', content: varcontent}]).then() :
                                            callback(null, {result: varcontent, intent: 'null'});
                                })
                                .catch(err => {
                                    console.error('Something went wrong', err);
                                    return message ? message.reply([{type: 'text', content: 'Something went wrong' + err}]) :
                                            callback(err, null);
                                });
                    }
                } else {
                    return message ? message.reply([{type: 'text', content: varcontent}]) :
                            callback(null, {result: varcontent, intent: 'null'});
                }
            })
            .catch(err => {
                console.error('Something went wrong', err);
                return message ? message.reply([{type: 'text', content: 'Something went wrong' + err}]) :
                        callback(err, null);
            });
}

Gestion des exceptions

Il est primordial que toutes les éventuelles exceptions soient correctement gérées afin que dans tous les cas, une réponse soit renvoyée, même s'il s'agit d'un message d'erreur (plus ou moins "naturel" ;), sans quoi la requête HTTP qui a appelée votre bot restera bloquée en attente d'une réponse...

Exploiter au mieux l'API Message

La plateforme Recast.ai met à disposition une API très utile permettant de créer automatiquement une réponse à un message issu d'une conversation via une messagerie instantanée (Tweeter, Slack...) tout en préservant les notions de conversation, réponse à un utilisateur spécifique, contexte du message etc, tout ceci de manière transparente via l'utilisation de la méthode message.reply(). Ainsi, quelle que soit l'origine du message ou sa forme, votre contenu sera toujours correctement interprété par la plateforme cible.

Vous avez maintenant tout ce qu'il faut pour mettre en place une interface naturelle dans votre projet!


Fichier(s) joint(s) :



Connecter une machine industrielle à Cumulocity

Il est clair aujourd'hui que c'est l'Industrie qui va drainer le plus de marché (et d'innovation) dans le domaine de l'internet des objets. Pour s'inscrire dans cette mouvance, nous allons voir comment il est possible de connecter une machine industrielle à la plateforme IoT Cumulocity.

Le protocole OPC/UA

Il s'agit d'une extension du protocole OPC, standard de l'industrie permettant la communication entre machines, qui vise à simplifier leur intégration et leur utilisation en apportant notamment de la flexibilité, de la sécurité et de l'indépendance vis-à-vis des fabricants.

Rapide point d'architecture : avec OPC/UA, chaque machine est reliée à un serveur (souvent 1 machine = 1 serveur, installé en local). Ensuite, une passerelle permet d'interroger les données des serveurs ou envoyer des opérations :

source : https://ewon.biz/opc-ua-server

Serveurs de démo

Pour tester les fonctionnalités d'OPC/UA, il est possible d'utiliser un serveur public, comme par ex ceux d'OPCLabs, ou d'installer son propre serveur local, comme ceux fournis par Unified Automation.

Client

Le client le plus pratique à utiliser pour des premiers tests est UaExpert d'OPCLabs téléchargeable ici. C'est lui que nous utiliserons pour notre démonstration.

Premier cas d'usage

Description

Pour les besoins de cet article, nous allons nous appuyer sur les données de démo contenues dans le serveur présenté plus haut, représentant une chaudière (boiler), simulée donc par le serveur. L'idée principale est qu'une nouvelle donnée et un nouvel évènement soient générés côté Cumulocity chaque fois que la température de notre chaudière change. Voici l'architecture mise en place pour créer le lien jusqu'à Cumulocity :

Le lien vers la plateforme est réalisé par un Agent (fourni par Cumulocity) déployé sur la passerelle. Pour le récupérer et le configurer, il suffit de suivre la documentation ici.

Configuration des Devices

Voici à quoi ressemble la configuration dans Cumulocity du Device correspondant à notre passerelle :

Comme vous pouvez le voir, il est lié à un autre Device, représentant le capteur de température. Voici sa configuration :

Comme vous le constatez, nous avons configuré ce Device pour émettre un Measurement et un Event à chaque changement de valeur. Nous aurions pu également lui dire de lever une Alarme. On retrouve également à chaque fois le "Browse Path" qui permet de naviguer (à la mode OPC/UA) jusqu'à la valeur à surveiller. Cette information peut se trouver grâce au client UaExpert, dans la vue Attributes, sous le nom de BrowseName :

Testons!

Maintenant que tout est configuré, il ne nous reste plus qu'à tester! Pour cela, il faut tout d'abord démarrer l'Agent Cumulocity (voir doc). Si tout a bien été préparé, il n'y aura pas d'erreur dans la console ni d'Alarme levée sur la plateforme.

Nous pouvons maintenant, à l'aide d'UaExpert, simuler un changement de température dans notre chaudière via l'appel à la méthode dédiée "Heat" :

Une fois la méthode invoquée, nous verrons apparaitre dans Cumulocity nos valeurs de température ainsi que nos évènements :

Hope this helps!


Fichier(s) joint(s) :



Interpréter des messages Sigfox avec Cumulocity

Sigfox propose, depuis son "backend", de configurer une adresse de callback où seront transférés tous les messages émis par les objets :

De son côté, Cumulocity dispose nativement d'une brique technique (appelée Agent), capable de recevoir un message issu du réseau Sigfox et de l'interpréter, pour par exemple créer automatiquement un Device s'il n'existe pas dans l'Inventaire, lui rattacher de nouveaux messages, ou transformer les erreurs (au sens Sigfox, via la configuration de callback d'erreur) en Alarmes. Il est également possible d'envoyer des commandes vers l'objet via l'interface de Cumulocity.

Leur documentation pour la configuration de l'Agent étant très claire et précise, je vous laisse la parcourir de vous-mêmes : Configuring SIGFOX devices for Cumulocity

Une fois tout cela mis en place, les messages reçus du réseau Sigfox apparaissent en tant qu'Evènements rattachés au Device dans Cumulocity, dont le contenu correspond à la structure JSON configurée dans le callback Sigfox :

Comme vous pouvez le constater, l'évènement contient une propriété "data" qui porte les données brutes présentes dans le message Sigfox. Il faudrait maintenant pouvoir interpréter à la volée cette information pour récupérer les valeurs que nous avons stockées dans notre message, puis créer automatiquement des vrais Relevés de données (Measurements). Ceci dans le but, par exemple, de faciliter l'intégration avec des applications clientes qui seraient à l'écoute de données en temps réel issues de capteurs ou permettre l'exécution des moteurs de règles de Cumulocity pour déclencher des alarmes en fonction des valeurs reçues.

Pour ce faire, nous allons utiliser le moteur de traitement d'évènements complexes fourni par Cumulocity (Complex Event Processing).

Ainsi, dès la réception d'un message, nous allons parcourir le contenu de "data" et extraire les informations issues de nos capteurs :

Quelques explications :

Avec le langage fourni par Cumulocity (Complex Event Language), nous sélectionnons tous les évènements en temps réel issus de l'Agent Sigfox :


select * from EventCreated event where getObject(event, "com_sigfox_SigFoxData") is not null;

Puis pour chacun nous créons notre propre structure de données que nous insérons en tant que Relevé (Measurement) :


insert into CreateMeasurement select ...

Le concept de Fragment dans Cumulocity nous permet de recréer une structure JSON composée de plusieurs valeurs. Vous pouvez voir le résultat à la réception d'un message à droite de la capture d'écran, ainsi que dans le dashboard du Device :


Fichier(s) joint(s) :



Arduino et infrarouge

Aujourd'hui, un article rapide sous forme de mémo, pour mettre en place une communication série entre deux Arduinos en infrarouge. Ceci pour permettre de trouver rapidement le matériel et code nécessaires.

Avant tout, il faut savoir que dès lors qu'une communication série filaire (USB par ex) a été mise en place, il suffit de "remplacer" le fil par une liaison optique infrarouge, puisque l'encodage reste le même (UART pour de la communication série).

Code et montage

Puisqu'il est inutile de réinventer la roue, voici un très bon tutoriel de Zola lab expliquant quel montage et quel code utiliser pour mettre en place la liaison optique :

Matériel

Et pour se fournir le bon matériel pour réaliser ce montage, rendez-vous chez Lextronic :

Have fun!


Fichier(s) joint(s) :