Tracking dans une application CodeIgniter

Introduction

Le tracking est utilisé par Google, Xiti, etc. pour tracer la navigation d’une personne depuis sont PC. Par exemple, les recherches faite sur Google pour trouver un livre seront stockées et réutilisées pour vous proposer des produits annexes (livres du même auteur, du même genre, autres livres achetés par ceux qui ont acheté le livre recherché, etc.).

L’objectif de cet article est de mettre en place un système de tracking (= stockage de navigation), dans une application web en CodeIgniter, sur des actions ciblées (clic sur des boutons ou des liens). Une fois ces actions stockées, il faudra faire une analyse des pages consultées et adapter les pages aux actions réalisées = « mettre en pratique l’expérience utilisateur ».

 

L’UX Design

Cette discipline revient à adapter l’environnement des utilisateurs (les écrans, les boutons) selon la façon dont ils évoluent.

L’interface utilisateur mets en avant la marque sur le produit, comme une bouteille de liquide classique. L’expérience utilisateur est une bouteille retournée. L’utilisation du produit est facilité.

 

Déléguer le tracking ailleurs

L’application utilise un serveur distant pour intégrer des données de tracking.

Une simple application de tracking (sur un serveur de tracking) permet de stocker une trace à chaque fois qu’on appelle un WebService (= une URL) :

  • Quel utilisateur est suivi (son identifiant suffit)
  • Quelle application est tracée (un code unique)
  • La date et heure d’enregistrement
  • Une codification qui décrit l’action, l’écran, le bouton ou le lien

En utilisant le générateur de code, cette application est rapidement créée. Les écrans sont simples, mais suffisants. Cette application externalisée peut être utilisée pour tracer d’autres applications, faire des campagnes avec remise à zéro des traces, etc.

Il reste à créer le WebService qui sera appelé pour créer une trace.

 

Permettre l’ajout d’une trace

Dans l’application de tracking, j’insère le code qui permet d’ajouter une trace dans la base de données, avec 3 paramètres :

  • l’identifiant de l’utilisateur dans son application (inconnu ailleurs)
  • le code de l’application (connu par l’application tracée et par cette application de tracking)
  • la codification qui décrit l’action

La date et heure sera automatiquement insérée à chaque appel.

Code :

	public function append($trcidusr, $applbcde, $trclbmar){

	    // 1. Retrouver l'application selon le code qui a été fourni
	    $applications = $this->applicationservice->getAllBy_applbcde($this->db, $applbcde);
	    if( count($applications) == 1 ){
	        $application = end($applications);
	    }else{
	        $data['data'] = ["status" => 404,
	            "message" => "Unknown application where code = " . $applbcde
	        ];
	        $this->load->view('json/jsonifyData_view', $data);
	        return;
	    }

	    // 2. Ajouter une trace avec date et heure = maintenant
	    $model = new TraceModel();
	    $model->trcidusr = $trcidusr;
	    $model->trctisto = date('Y-m-d G:i:s');
	    $model->trcidapp = $application->appidapp;
	    $model->trclbmar = $trclbmar;
	    $this->traceservice->insertNew($this->db, $model);

	    // 3. Retourner la trace insérée, au format JSON
	    $data['trace'] = $model;
	    $this->load->view('json/jsonifyData_view', $data);

	}

Ce WebService sera appelé en GET, selon l’URL suivante :

http://<<server>>/index.php/trace/createtracejson/append/1/DIET-IMPULSE/welcome.index.success

 

Appel du WebService dans votre application

Dans votre application CodeIgniter, il faut ajouter la bibliothèque permettant de lancer des requêtes HTTP. J’ai réussi à intégrer Curl (https://github.com/philsturgeon/codeigniter-curl) mais ce n’est plus maintenu. D’autres bibliothèques sont disponibles.

En plus, il faut ajouter un « helper » qui mettra les bons paramètres via un simple appel de fonction :

if (!function_exists('trackData')) {
    function trackData($controller, $codeMarquage){
        // avoid tracking on localhost
        if( strpos(base_url(), "localhost") > 0 ){
            return true;
        }

        $controller->load->library('curl');
        $user_id = 0;
        if( $controller->session->userdata('user_id') ){
            $user_id = $controller->session->userdata('user_id');
        }
        if($codeMarquage[0] != '/'){
            $codeMarquage = '/' . $codeMarquage;
        }

        log_message('debug', TRACKING_URL . $user_id . TRACKING_APP_CODE . $codeMarquage);
        try {
            return $controller->curl->simple_get(TRACKING_URL . $user_id . TRACKING_APP_CODE . $codeMarquage);
        }  catch (Exception $e) {
            log_message('error', $e);
        } finally {

        }
	}
}

 

Notes :

  1. En mode « local », il est inutile de tracer.
  2. En cas de plantage de l’appel au WebService, loguer une erreur.
  3. On récupère l’identifiant de l’utilisateur dans la session.
  4. 2 données sont stockées dans les constantes de l’application : le code de l’application, l’URL de l’application de tracking

 

Dans le contrôler, on trace l’authentification, point de démarrage du tracking pour cet utilisateur :

$this->session->set_userdata('user_id', $userID);
log_message('debug','[welcome.php] : user connected: '.$userID);
trackData($this,'/welcome.index.success');

 

Analyses

C’est à vous de jouer !

Avec les données recueillies, il faut maintenant comprendre comment les utilisateurs interagissent dans votre application.

  • Est-ce que le parcours est optimum ? (le temps entre chaque action permet de suivre les clics)
  • Est-ce qu’une fonctionnalité est inutilisée ? (pas ou peu de tracking dessus)

Corrigez votre application, purgez les données stockées pour votre application et refaites une campagne de tracking pour avoir le résultat.

 

Envoi de SMS par un robot

Le principe, les outils

Je désire envoyer un SMS à tous les adhérents de mon association, pour leur communiquer une date d’un évènement sportif. Le mail ne fait plus d’effet ; il n’est pas lu, pas ouvert, pas arrivé…

J’opte pour un petit programme qui enverra en masse les SMS par mon smartphone. L’outil SMSGatewayMe me propose cette solution, gratuite, avec une API en PHP et une application à installer sur mon Smartphone.

 

La solution en détails

Depuis mon PC, mon programme (= mon robot) contacte par WebService la plateforme SMSGatewayMe, avec des messages du type « Envoyer le texto ‘Salut, il y a un barbecue ce week-end. tchao ! (association AAA)’ à ’06.12.34.56.78′, depuis mon smartphone, id=13245 ».

La plateforme SMSGatewayMe stocke cette info dans sa base de données, avec l’id du smartphone, le message, le destinataire.

L’application sur mon smartphone contacte régulièrement la plateforme SMSGatewayMe : « Est-ce qu’il y a des SMS à envoyer pour le smartphone id=12345 ?« . Elle lui retourne en réponse le SMS à envoyer.

L’application sur mon smartphone crée un nouveau SMS, visible depuis mes messages de mon smartphone. Les personnes me répondent directement.

 

La technique

Après avoir créé un compte sur https://smsgateway.me/, il faut récupérer le fichier « smsGateway.php ». Regardons le programme « run.php » :

<?php
include "smsGateway.php";

function main(){
  $smsGateway = new SmsGateway('moi-meme@gmail.com', 'monMot2Passe');
  $deviceID = 12345;

  $message = "Salut,
il y a un barbecue ce week-end.
tchao !
(association AAA)";

  $row = 1;
  if (($handle = fopen("envoi-sms.csv", "r")) !== FALSE) {
    while (($data = fgetcsv($handle, 1000, ";")) !== FALSE) {
      //echo "----". $data . "\n";
      $nbChamps = count($data);
      //echo "$nbChamps champs dans la ligne #$row\n";
      if($nbChamps < 2) {
        echo "fini\n";
        return;
      }
      $user = $data[0];
      $adresse = $data[1];
      $tel = $data[2];
      if( $user == "Nom" ){
        //echo "ligne d'entete\n";
        continue;
      }
      echo "Envoi d'un SMS vers ".$user." --> ".$tel."\n";
      $result = $smsGateway->sendMessageToNumber($tel, $message, $deviceID);

      $row++;
    }
    fclose($handle);
  }
  echo "--\n";

}

main();
?>

Le fichier « envoi-sms.csv » est un tableur Excel exporté en CSV, avec les colonnes suivantes : « Nom », « Adresse », « Téléphone ». Il y a une ligne d’entête pour mieux se repérer.

Enfin, une fois que tout est prêt, il suffit de lancer la commande suivante :

php run.php

 

Remarques

Ok, l’évènement n’est pas très sportif, passons…

Le nombre de caractères est limité. Faites des tests pour vous assurer que les sauts de ligne sont correctement placés, les caractères spécieux sont bien interprétés, le compte est bien configuré, etc.

Génération de PDF avec CodeIgniter

Introduction

Dans votre application, vous désirez générer un fichier PDF avec vos données, votre charte graphique, etc. Voici un exemple d’utilisation d’une bibliothèque, qui permet de faire :

  • une entête
  • un pied de page
  • ajouter une pagination (numéro de la page en cours ; nombre de page du document)
  • insérer des images
  • insérer du texte HTML

 http://hulan.info/item/html2pdf-convert-your-xhtml-to-pdf-easily

 

Démarrage

Voici comment procéder pour installer cette bibliothèque dans votre projet :

  • Copier dans application/libraries les fichiers :
    • Fpdf_rotate.php
    • Fpdf.php
    • Pdf_html.php
    • Pdf.php
  • Copier dans application/third_party les fichiers :
    • tout le répertoire « fpdf/fonts »
  • Modifier le fichier application/config/config.php avec ces lignes :
//FPDF FONT DIRECTORY
$config['fonts_path'] = APPPATH.'third_party/fpdf/fonts/';

 

 

Générer le PDF depuis votre controller

Définir le point d’entrée du controller (classique CodeIgniter), par exemple « getAttestation() » :

public function getAttestation(){

 

Définir le répertoire des polices :

define('FPDF_FONTPATH',$this->config->item('fonts_path'));

 

Charger les bibliothèques :

$this->load->library(array('fpdf','fpdf_rotate','pdf', 'pdf_html'));

 

Mettre du contenu dans le PDF :

$pdf = new PDF_HTML();

$pdf->Open();
$pdf->SetTopMargin(10);
$pdf->AliasNbPages();
$pdf->AddPage();
$pdf->SetFont('Arial', '', 12);
$pdf->SetDrawColor(0);

$pdf->SetTitle('Attestation');
$pdf->Ln(5);
$pdf->SetFontSize(16);
$pdf->x = 70;
$pdf->Write(0, utf8_decode( "OK") );
$pdf->Output('', 'I'); // I : inline file

 

et terminer la fonction « getAttestation() »:

}

Lorsqu’on lance l’URL de ce controller dans le navigateur, un fichier PDF est généré :

 

Le fichier a pour titre « Attestation », comme prévu par le code.

CodeIgniter + React JS

Introduction

ReactJS est une bibliothèque prometteuse pour changer la façon dont les applications web sont conçues. J’ai retenu une chose importante, ce qui fait la différence par rapport à un appel AJAX simple et classique, c’est que React compare le DOM de la page avant l’appel et après l’appel pour appliquer les changements uniquement sur les modifications.

Il est donc inutile de faire recharger une page complète en cas d’ajout d’item dans une liste, ou même de l’ajouter « à la main » (avec un « append » en jQuery). React s’occupe de faire ces changements.

Soudain, on a envie de passer par cette bibliothèque pour tous les petites modifications à faire sur un écran (notification, compteur) et en exploitant cette technologie à son paroxysme, toute l’application y passe.

 

Quels impacts pour une application MVC ?

Si on prend React avec le framework « Flux », le MVC est mort. Dommage ! Faut-il recoder mes applications avec un autre paradigme ? Pas sûr.

Par contre, une application CodeIgniter qui désire utiliser React doit fondamentalement revoir la signification du V, pour la « Vue ».

En théorie, la vue, c’est ce que que les utilisateurs « voient », donc le HTML qui résulte du serveur hébergeant l’application.

Avec React, la Vue au sens HTML n’est plus à gérer par l’application. React s’en chargera.

Version classique du MVC de CodeIgniter (ou d’autres frameworks)

 

Casser la vue !

Oui, il faut casser la vue. L’application CodeIgniter ne fera que des interactions en manipulant des données JSON, et n’ayant que pour interface technique des WebServices REST.

Une autre application pourra se charger de la vue, des interactions utilisateur et des actions vers le serveur (liste des items, ajout, modification, suppression d’un item). Elle appellera l’application CodeIgniter pour le stockage en base de données, les règles métier, etc.

La partie « vue » de l’application est supportée par une autre application dédiée à l’interaction utilisateur. L’avantage est que si vous avez une application mobile à réaliser, elle pourra interagir avec cette application CodeIgniter en utilisant les mêmes WebServices.

Avec React, la vue peut supportée par une autre application.

Conclusion

La question qu’on peut se poser est la suivante : est-ce utile de s’appuyer sur un Framework MVC pour mon application s’il n’y a plus de Vue ?

S’il y a du boulot côté Modèle ou Controller (ou service) = des objets complexes, des règles métiers, la réponse est oui.

Si c’est une représentation HTML des objets simples en base de données, avec peu de règles métier, la réponse est non.

Données d’un capteur vers un serveur Web

Introduction

Lors de mon précédent article, j’évoque l’envoi des données via Wifi depuis un ensemble de composants autour d’une carte Arduino. Or, la programmation n’est pas très facile pour donner plus d’intelligence à ces capteurs. Je suis donc reparti du capteur de température et de pression DHT11, mais relié à un Raspberry PI.


DHT11 - Cảm biến nhiệt độ và độ ẩm https://images.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.openmediacentre.com.au%2Ffileadmin%2Fuser_upload%2Ftx_onqcatalogue%2Fproduct%2Fraspberry-pi-model-b_80_thumbimg1.jpg&f=1
(les photos ne sont pas à l’échelle)
Prix des composants :

  • Raspberry Pi 2 starter kit (avec les câbles, l’alimentation, une carte SD, des câbles, des radiateurs, un boitier, etc) : 75 € sur Amazon
  • DHT11 (capteur nu) : 2.20 € sur Amazon
  • Dongle Wifi (sur port USB) : quelques Euros (sur eBay)

La programmation est faite en script SH et Python dans le Raspberry et je dispose d’un serveur PHP qui héberge les données (mon PC).

 

Vue du Raspberry avec les câbles pour clavier, souris, écran (inutiles en mode normal, mais nécessaire pour la programmation) + le composant DHT11, relié par 3 fils.

 

Vue des composants minimum pour que les données soient envoyées après avoir tout programmé. Il faudra juste prévoir une boite pour protéger le Raspberry et laisser le capteur « à l’air libre ».

 

Protocole de communication – 1. Souscription

Le Raspberry dispose d’un Dongle Wifi pour se connecter au serveur. J’utilise donc l’adresse MAC du Wifi pour souscrire au serveur selon ce principe :

Dans ces échanges, l’adresse MAC sert de clé. Chaque unité de communication devrait avoir une adresse MAC unique sur le réseau.

Une fois le capteur identifié sur le serveur, il n’est pas encore actif. Le serveur a un capteur en attente de paramétrage manuel (donner un nom compréhensible par un humain, un picto, un groupe, une catégorie, etc…). Tant que cette étape n’est pas faite, les données ne sont pas transmises. Le capteur passe à l’état « Activé » lorsque le paramétrage est terminé.

 

Protocole de communication – 2. Envoi des données

Le serveur est maintenant prêt à réceptionner les données du capteur.

Dans le cas du DHT11, il y a 2 données à chaque envoi : température (en °C) et humidité (en %). Le Raspberry ne fait qu’un appel au serveur pour ces 2 informations.

 

Montage électronique

Le montage sur les PINs du Raspberry sont faits directement car le DHT11 accepte un voltage de 3.3v, fourni par le PIN numéro 1. Si cette alimentation directe n’est pas assez stable, on pourra utiliser un régulateur de tension (AMS1117, 4.70 € les 10 pièces sur Amazon), prenant en entrée du 5v et donnant en sortie du 3.3v stabilisé (monté avec un condensateur de 470µF). Ce sera surtout le cas si on utilise une autre source d’énergie qu’une prise de courant (pile, accu, batterie de voiture, capteurs solaires, etc).

 

Le DHT11 comme capteur

Le DHT11 est un composant assez instable, qui ne garanti pas toujours la bonne lecture des données. Il semble que d’autres capteurs soient plus efficaces, peu onéreux, fonctionnant avec une plus grande plage de voltage : TMP36GT9, DS18B20, etc.

Le DHT11 est par contre bien documenté, pour les bidouilleurs comme moi, devant programmer en Python.

 

Copies d’écran du serveur

Liste des capteurs

 

 

Détail d’un capteur d’humidité

 

Pour aller plus loin

A partir de ce système, il est possible d’équiper toute la maison, d’avoir des capteurs partout et de suivre les courbes. Le budget à prévoir reste collé au prix du Raspberry (carte nue à 40 €), car les capteurs ne coûtent que quelques Euros. D’autres solutions voient le jour, avec des cartes électroniques programmables, comme TESSEL (avec Wifi intégré) : https://www.tessel.io/

D’autres capteurs pourront être ajoutés, sous réserve de pouvoir lire les données (sur le même principe), depuis les ports GPIO du Raspberry. Grâce au grand nombre de PINs disponibles, on pourrait ajouter d’autres capteurs à un même Raspberry, recueillant ainsi un lot de données. Pour une pièce de la maison : température, humidité, ensoleillement, concentration CO2, odeur (oui, pour faire remonter le niveau de puanteur du placard à chaussures), méthane, détecteur de mouvement, etc.

 

Codeigniter 3 chez free.fr

Codeigniter 3

J’utilise le framework Codeigniter pour mes projets personnels, avec mon générateur de code. Sa simplicité d’utilisation et sa rapidité de mise en œuvre restent un atout majeur pour de petites applications web.

En voulant faire une requête de groupe, comme ceci, je me suis confronté à un problème :

select a, b from table1, table2
where table1.c1 = table2.c1
and c1 > 10
or (c2 > 20 and c3 > 30);

 

Les opérations avec les parenthèses sont réalisables de 2 façons :

  1. Faire la requête en SQL pure et la lancer dans CodeIgniter. C’est assez moche et doit être réservé à des requêtes complexes, visant à faire travailler le moteur du SGBD. La requête sera travaillée dans un MySQL Workbench pour être sûr du résultat. On restera alors sur une base de donnée d’un certain type (ex : MySQL), sans pouvoir passer un jour à un autre type (ex: SQLite).
  2. Passer à CodeIgniter 3 pour utiliser la fonctionnalité de regroupements dans les requêtes : voir la documentation dans CodeIgniter

Cette seconde méthode exploite pleinement le SQLBD tout en utilisant le QueryBuilder du Framework. Sauf que pour ça, il faut passer à CodeIgniter 3.

 

Et free dans tout ça ?

L’hébergeur free.fr est gratuit, oui. Mais il est aussi :

  • lent au chargement
  • limité dans l’accès FTP sur le nombre de sessions ouvertes, ce qui est assez pénalisant à l’usage
  • limité dans les bases de données par site (1 seule)
  • ancien, dans les techno qu’il nous mets à disposition
  • permissif, non sécurisé (piratage de compte vécu)
  • sans support (newsgroup étant le seul media d’échange, il reste sans réponse)

Pour que CodeIgniter 3 fonctionne, il doit utiliser PHP 5.2 minimum. Or, chez free, il est à la version 5.1 – dommage !

Pour ces multiples raisons, j’ai ouvert un autre site chez WebHost et j’ai fait un test chez eux.

 

Résultats

A part l’interface de gestion assez « old-scool », on retrouve ses repères. Ouverture de site, ouverture de compte mail, création de base de données MySQL, accès FTP : tout ça se fait très facilement et rapidement.

Mon choix est fait, adieu free, enfin pas pour tout. Je garde mon blog, même si je me sens dans une toute petite boite, sans pouvoir étendre mes jambes ou mes bras (version de WordPress assez ancienne, ne pouvant pas se mettre à jour, pas d’accès aux serveurs externes pour mettre un captcha, etc…).

 

Arduino + ESP8266 + DHT11 : relevé de température vers un serveur HTTP

Introduction

Suite au précédent article qui a permis de faire un HTTP GET sur un serveur en Wifi, grâce au module ESP8266, j’ai ajouté un détecteur de température : le module DHT11, au prix de 2.35 €.

data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0NDh0VFhEYIx8lJCIfIiEmKzcvJik0KSEiMEExNDk7Pj4+JS5ESUM8SDc9Pjv/2wBDAQoLCw4NDhwQEBw7KCIoOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozv/wAARCAEsASwDASIAAhEBAxEB/8QAHAABAAICAwEAAAAAAAAAAAAAAAUGAwQBAgcI/8QAPBAAAQMDAgQEAwcEAAYDAQAAAQACAwQFERIhBjFBURMiYXEUMoEHI0JSkcHRFWKhsSQzQ3KC4RYl8PH/xAAaAQEAAwEBAQAAAAAAAAAAAAAAAQIEAwUG/8QAIhEAAgICAwADAQEBAAAAAAAAAAECEQMSBCExEyJBUWEU/9oADAMBAAIRAxEAPwD2ZERAEREAREQBERAEREAREQBERAEREAREQBERAEXBTKA5RcZXKAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiLjKA4cQ0Ek4AVaud8eZy2neWtaeY6rYv8AdBGw0sTtz85H+lVnvyea9Di8e1vI8vmcpp6QLPb+Iw7EdWMf3j91PMlbK0PY4OaeRC84DyOqkLfdZ6J40Oy3q08ir5uIn3Apg5zXUy9ItK33KG4RameVzfmaei3F5rTi6Z60ZKStHKIigkIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgOFoXa4NoaYkH7x2zQtqeZlPE6WR2GtGVSrlXPrKl0jicdB2C08bD8krfiMnKz/FGl6zVnmdI8uccknJKwFckrple0lR4DduzsEkkEUeoo3GNzj3WUx7YcAQVANeycRGjubXvJ8MnDx6L0ZtdTGmFQJmmI8nZXl9XZmOf4tLiN45t6H+Fv0uuCnEbnknmd9srJm4yySs3YOU8UdX2ejRTRzM1xvDmnqF3VEo7s+3zNIkw0nzN6FXenmZUQMlYcteMhefmwvEz0+PyFmX+mVERcDSEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBcFFAcXcQssVqcWyAVMoIj/tHVx9v94QG27iWzsrzRSV0UcwcWYecAuHMAnZSYe0jIcCPdfNNdWyVlUZiXY5NBOSB/KnLZxFxJYYYn/fOpH40smaS0+x6KVCTDcV+nrHENYDO2lL9I059Cq/KwtO61a26SVlLDVTRGGUx+ZhdnT9VHUl/PxbaN7TJrOG4XtceOuNI+f5UtsrolHBdMLK/nsuG4DTkc1oMhDXav8ADYYozuubRepcCGca2pXWkvcZITn+0rrbqHD8uGA3muMnLbo7wjHW2TpcHt1tzg910C41aRnOAFhp7lTTzugLg145LqcfWaFyZVxO8YNL4x1b091PcMcUy00TaaVhljHLu1dHNLefJa7IYYXufGwNLueFznjjkVM64sssTuJaYeJz8QfFjHhk7Y5hT1PUR1MQlidqaeq82qKltOzUTv0UzwlxBF4rqWeQND92EnbKw8jjRUdono8XlylLWbLsixumjYGl72tDuWTzXcHO4Xnnp2couFyhIREQBERAEREAREQBERAEREAREQBERAERcIDFV1UNFSyVNQ8MiiaXOcegC8G4y4jmvd0le4kNzhrPyNHJv7n19lb/ALSuK9za6V4LInecg/M/t7N/37LzqhtFxushbRUks7ubnNbsPcqUrZPhv8J2QXevMkwzT0+HPH5j0C9DlDS3QWNLRjykbeirPDtmvFidMayB0DZQCDqBBI9vdT7LhDO7RIQ1/dexxktLPC5038tGhdKad0bpIMvIHydVr2S2GlYauob/AMRL0P4B2UxIMdVjzlaaMOx0mmELDITy5LHR3OCqzHIQx4691pXR0uDlpDeQxyURCx5mBGc52wucp06OkMbkrLe9mn26FY+qx0wfHA1r3ZP+lla3UcNO+OXVdEcn6aVfK9rA0Ahp5uwoeueY6aSpgjYx8YHhg8yc8z3VlcwEYcMjqCo6qtTZ5WObJpjz52Y5j0VJxcvDrikovs68O1FyloNVwIO/l/uHdSoBcdljaA1oDRhoHLsom4XGQTAU7gNJ5lSlrGisntK0S81PHUMMcrcj/IUWbXPSzB0DjIwn6tW3RXilncIaiRrKhwzpzvhbj9twchE1ImnBnf4mbwmMklc/QMDJUpbeIJ6QhkpMkXYncKDyTzXIOOqrLFCSpovDNkjLZM9Eo6+Ctj1wvB7jqFsrzylqpaeQSRPLXDsrhZrp8fEWyACVnP1Xl5+M8fa8PX4/KWXp+koi4yuVlNoREQBERAEREAREQBERAEREAREQBV7jDiJthtTjG8CpmBEf9o6u+n+8Kbq6mGipZKmd4ZFE0uc49AF4JxjxFNfLtLIdQadgz8rRyb+59fZPeiUYbTRu4k4higeXNjcS5x6hg3J9z/sr0CeCM0LaODVSws+WOM4GPXuq1wXa5KVpudQXMdKwtiaPy91ZHuycg5C9PBx1VyR5XK5bT1gzQneaKlEQLi0dSVBxsluNxEULyxrDqc4dFZn6XtLXNDgeYKwU9LBRtcIGadZyVuUUlSPNlNydy9M48rAwEkAY3XLA2TIY4Et5jqtWqqPBjwPndy9FBtrpIqjXG4g57pKSRSMXLwsr2AtLXDIPQrUjoYYZvEaPYdllpK74uLzsw4c3BZDuU6fYtx6OskgiYXu5BaFJM2pqTUyyPjhjeGFzCM5PLbqtypp21EZjcSOxCWywMqqeamlnjYcZZnbUf/wXPK2o2jrgipTpqzO6rjFQaWV2ZfwnGCR6hCtGktz6Wokmqah9RP8AIHPdq0tHQFbE1Q2BmojJ6BXhetspkS2pHd7NcZbkjPUKKmt7adr5JCXADOrKm4yydgc0FjiM6HDB+i6Pbza4ZHYqX9iE9WVu12uKsuX9TOdLPlAOxKsZcsbWMjaGxtDWjkAMALnKRio+CUnL055oAcoAsgAaMkKzIOW5bsOZUja6w0dUx4O2d/UKPG255rlrsFZMrUlRuwQcPsejscHsDmnIIyF2UZYZZJbYwyZ2OBnspNeQ1To9qLtWERFBIREQBERAEREAREQBERAFxlcqvcYcQssNpc5jwKmYFsX9vdx9B/vCAqH2mcVgE2qlk8sR+9x+J/b2HP3wqJw3Z3Xe4a5QfAiOqQ9+w+q0pJJrvc2RtJLpXhrNR7nmfqcr0q32yK026OlhHIZc7HzO6lbOLh2dsycvP8cKXrMjiGgNaAGjYAdFiLl2esfVeuj5+77O2VzjK6chk7AKJqOI4aOvjie5pZIdIaeqhtIsk30iTqaRlQ0h2QcYyFDutUsc4GNQJ2cFPwzQVUWuFwO27TzC6u2VXFSLRm4GKGJsMQjb0/ytOvvMNtc3xCCM+bK3JS8ROLBl2NgvPr9LNJWeHNqaeZBVMuT442WxweSR6JSVNNcIRJTSA5GdOd12OWlef8LCsfchHTSObEzzSE8gP5V/1akxT+SN0RlhpKkcZPPmoWpnqZbhGItTHscC33U6GHTqG4PVdQ0CRsmka28nY5LpJWikXUk2dbhxDWzUzKSqpWiVm0bo4/mPqc7Luxz3RN8Y5fjchblfcG10UAdTxsfEDlzfxLSfI2KMyO5BcsUdY/w7cialLp2dvDPPoupbuoFt9mhrXOwTETyVgpaiCtjDmOAdzwV0jNPw5OEl6ctHddue/Qcl3czGyxl3RUySo7YceztnJK27XQPuFY2NoOkbuPYLUiY+aVsbGkuccABXu0W1ltpGswDI7d59Vgy5NV16epjx7P8Aw3IYmQRNiYMNaMBZFwuVhNwREQBERAEREAREQBERAFwuVwThAYauqhoqWWpneGRRNLnOPQBeC8ZcRzXu6SSOOlp2azPyN6N/c+vsrj9pnFYLjaqV+WRO+9IOz39vZvM+uFUeF+HmXBzq6vZrgyQ1pJGs9T7LpixvJKkUyZI4o7SK5SeL8XEYATIHjQB3zsvTBcHUbQ2c6h1HPC0afhyit9c6spmuJx5I3HIYe4Ufd6xzst0nxCcAL1ePieNOzxuVmjma1LOyaCqjEkDw4Hp1C6lq0LHQOt1F94SZZPM8Z+X0W++eGPAkdpJOy0mF+mrXtmdTObAMuxyzzXnD7fWXW4TGdxgljdjw3DdoXqjo9sjcHkQtGroIak5e0teOT2nBXPJDdHbDl+NtkVw5QVdLq+Im8SJoGgnY56qde9rRqc7A7rq0BjQ0DAAwoG+3TQ0xRuU2sceyjvJPosZZ5Q4EEHkRyWhcrTSXKLRUxgkcnjZw+qr1l4lqqZo+JjPw7naRJzb9eytwkbNG2RrS0OGcFVhOOVdFpwnifZoWy2w2ulEEXmOcueebisV6ubLfRudnDiFJY3UHf+H3XVgfDMWSN30u+VytK4x+pSP2l9mQlo4tqqWciYa4XHl2V3gqoa2BssOQCORVLs/DE7ppPi8wmM4aMZ1Hv7K4wQsgibFGMNaMBZ+POcr2/DvyYRi1RlWvWU76iPS1+MdO67SV9NBOIZTjPUdFt6AW62EOaeoWp0+jMrTtEE227+ZuCFGOrpKe4mOEHwmj5u5VtkgEsbmOyARjIOCq0+1Oo6nwyS9rtw49VnjicZWn0bHmWSFNdlhoqwVVMDncc13c7dadFCIGbbZ6KL4lvrbVRlkbgaiUYYPy+qpkkrs74Yuki/cHf0+pnmkbUxSVEJ0mIOGpnqQrgF8oQXCqpagVME8kcwOQ9jiDle5fZxxNebtbWC7ATFxxFIBhxaOrl5c5OTs9WMVFUi/IiKpYIiIAiIgCIiAIiIAiIgCr3GHETbBaXOY9oqpgWxA/h7uPoP4U3VVUVFTSVM7wyKJpc9x6ALwPjPiSW+XaWUktYfK1mfkYOTf3Pr7ISjUoaWbiS+Ni1u0ElznHchvU+5z+pXoghZTQsghYGRsAa1o6AKgcG1U1Le2mOMva9ha/Azgc/pyXpDZYqkb7OPVerxElC0ePz3KU0vw0ycLWkpaeWdk74mukZ8ruoW/NA5h7hazm4W48vtHR8gY0uccAKAvEss1O98cgDgcac7tHdT7gHAtIyFBXiyTT0U8dvk0OmILmk7H2VMm2v1OmLXb7eGHh7iCeaU0oBmDMBzvwkqzvwdwoWwWhlqo2tLfvCN8jdSNTUNp4i9x36KMeyj9icrjs9fDMW7e6r934cdPN8VRSaZRzjdu1ykLdfIarMcwLSDgPxhSb2DSHNILTyIUyjGaplYuUHaKxZaDxJnyvpn0pYdMkX4JD3AKsJeGtJOwC5cFE36qnpaIujic5vUtVMeKOGPRfJllmkrM8F+pHVjqWVwa78J6qUcwFuppDmnkQvM4WtkzUSPEzS7dmrEjSeo7q82mGalp8STPeDy1c8eq44uT8k9aO2XjOEFKzePNdJZfBhc8DJxsuxOSuFsMaf9K1PFLVTF7sklbtHcJbXpbJIcOOAD1UoaRhf4jNj1HQqs3COae6OM7CwR7Mb29VjUZxmb7xzh0XOCqjqofEbgeiwVDWyEauYOQou1eLDsTlpW/JIAC5xwAr5JdkYYKuzXuFfFbqOSolPlYOQ5k9l5fcbhNcaySpndlzjsOw6BW653VlTUGM7wg6S0jZw6lU+so3x3R9JG0udr0tAHNedlyW6R62HHqrZu8OWh95uscRafBacyEdu31X0RwvaWW+3scYwxxGGjHyhUv7OeExTwxukb8p1yn8zuy9QAAAA2Czs7nKIigBERAEREAREQBERAFwuVX+MeI2cPWh0jHj4mYFsI7HG7vYICn/AGncVhpNopn5bGczEfif0b7DmfXC8ubTVNRG+obE97GnzvAyAfVbDnTXi5AayS9xJe88hzLj/klWTh6x1F9q3zUNS6ipaJuiB/IuPc46ncn3wrJfrDZFw0LGQ00tnrWzVbAHTwh5Y5xznDQcagFdYqnMQcWlriM4OxCrV6ss1I//AO0o/BGry11I3yu9XNG36YK7U16FDRYrSamFpwyshOsH0cObT7rbx8kU2mY+Vjckmi301cflkAIKzPgZMNUJ+iiLKW3ssZQTNeZPlPb37KRqKOWzOayWRwkPQ9f/AEt3yRur7PMeGTV10Ynxlp5LERhbrKiGobhwAd3XSamLN+YPIhdUzLKLRpqKuL6hlSx5ZmIHnjIHupdzCDhdC3Iwd/dGrREXTspN2dO+4R0FBlssm79PNg6K40ML6WkZFJIXvA8xPUrBBaqOmrZKuKPTLJzW1LK2KMvceS544ONtnbLkU0kkZm6XO06xqP4eq4fHsQ4Z7gqvSuramfx6c4DckF2wOBnGe6kLVfmVsnws287RuBzarbq6KfHJR2o1qjhmilrY6qPMJa7U5jeTv4UsVlkAHLcd1j0lSoxTtIrKcmqZ1XdvquAMLu1qsVO4GnfmtS4UzKhrXY87eq2S7mf0WJ79lwnPs14sVq2YY2CJgAWrWvLoSAcDr6rS4ivbbTREtIM8m0be3qqdHxZcmsDHlkg/uasWbJXSPT4+JesnZqWMy+JIQyNnme49AtnhKxvvd7fc3sOl8hEI/dV2gdcOJbjHSPeRFnL2sGAB1XvPCNiioKZk/hacN0xN7DusLZtJy20MduomU8YAwNz3K20RQAiIgCIiAIiIAiIgCIuMoDFVVMNJTSVM7wyKJpc5x6ALwLjTiWW/XaSVxIjGzGfkZ0Hv1KuX2n8VhubPSvy1hzOQfmd0b7DmfovM6OLU59ZO3XHGdgfxvPIfufRErZPhlOmgo2wkkT1IDpSBuyPoPc8/bCm7Rea6lp3Pio2T29o/5LT52gfi23HqcLB/8WuNbRNuQAqDIS+VjD5+fToulVcmNAbTMkbUjDWte3BhIxk+m2ds464GF0ILO7iGKtomQ0FU6bxDh0UjfMBjJBPXksFVQUPD9tnuclPFpeAfBdv4kvRvsNzjv7KL4NpZ31bKgNY2ka5w1uG7sDJdnoB/A6qSbJTcW3aWona/+mW8BsUTjs8nOXOOfT/S6x8o5S9s2vs3qqCesqLnDRGjqWgRvbG7MRzvnB5HY/quL/eqmrvb6uUEU7zpiIOWFo5exU9T04PD01zoY2MgZG4t0gDcenZeaX6c0VNDPQHS0gB+ndkg7lp6q0Jay2/hWcdoU/0tjZ2hmphIJ3xnY/VZ6O9lr/DkGW+qp9kvlHURiB9R8JL0ZMcxOP8A3fh+v6q7mOwQ8LfFOqQ24M2cwODiSTsMdsdQtq5EKR58uLNs3g2KqZrhIz+VaskRaSCMFQ1PXyRBsjCC08iOqnKa4RVLQ2bZ35lpTMEoGuWrDPTsqIyx+ceikZqYgam+ZvcLVcwhW9OVNEJVOns9snMUAnfv4bw0k77bjkPdOHbP/TqYzT+aqn80hPT0UxyQLksSUtjvLPKUNDiaoFLEZHY9iuaKsprgzMTwH9W5UNdKkvnEb8sZnB26KI4kiHDVbSyUlZ4xljD8AFux7hRPKoOmWxYJZFaLx4RBwQurtvZRlmv3xsLWyu3I2JUhM8EkDkrSl0Ux47lR0e9RlyukNDTvle4eUZwsldV+EzSPmPJUPia5MqZWwRSFwbu/tlZJy1Vs9LHDZ0iMulxmuda+olPPZo7BazGlxAAyTyC6gbq1cFcPvute2dzcsjcNIxzd/wCl5zbfbPRSS6RePs04UdGPFnjw92HPPYdl62xoY0NaMADAC0rPbWWygZA0ebGXH1W+qFgiIgCIiAIiIAiIgCIiAKB4u4hZw/ZnzNc34mXLIQe/c+g5qammjp4XzSvDI2NLnOJ2AC8G474lkvl2e5riImjTGz8rP5PM/TsoJRXayqNdUySyyknJILty453z68yrBQwUzKikn0/F22Bvm8AanNeebntOCN/8BVQAudjurfScK/BywsF/jo7pIwPihLXN58hr6FdEuiGz1rhqK1S2EfBzR1MLiXFwGMHtjmOXVef8Q24y3T4l0DYaeZpMc78HUwblx9AP46qIgv1Vaa+WK4slpaljtD6ulwMn+9vyvH6LW4n4jfXRthZVsqXvYBJLGwtGkHZuDy7n6JXdj/CUlqI7vYDQ2Cojjl+R1I86JDGOQaTs7J3ON8rBwbJ8N8ZZq2Iw1D3awyVuCdsEb+w/yqU1zgcgkEb5HRTlLxXK6NlPeIG3KBnyvcdM8f8A2yc/1yrp92Va6o9Nb4bbGbdrEMRDgWRtJcR0b2xleX8UEUFOLe/GskYHUNyTk/VSc9RX10Ovh+/1FSDsaSZ2mob7fm9wVTqyOf4pwqxJ42rz+JnVn1yp8uirV1ZzR2yWqZ4oIjiBxqd1PoFM1Mk8NS9sYidqOW6zs4Ach6/wrI74azWe2zsgLmhjHSFr8Zzvj/8AZ3K1Kqjo+I6a43O2uc2Wlc2UwSAZDTs7lz3x0UeMn1EPa74wy+BJ90/PyuOysDJ5ahzY6cEvPJrdyfZUS7wllRG9wAe+MOIB5dlI8LcQ1dluUNUG+MyA6sFaoclx6kZJ8ZS7R6BQXSenIjnB7EFSv3NU0OiOCeYVbvPEzbvdG1HwrKdro25DeZd3P6rLTVZaQWPW3Hk2imzzsuHVuKJeSItOCFixhZqW5RVDQybnyys0tKcameZvcLrZlca8I6emiqYzHK0OB69QqrxDYaya6Gue4SwuAAwPkAGAFdPCwd10fpeC1w8uMYXPJCMu2dcOScXUSqUNKWlvh+XCmKmqdBANZ85HJdGMipvEfkaWlQd1ukcYM0xIGcNA5lc5ySX+I0Qi2/8ATUvV5dBE9g3kkGB6KoElziSckrYq6qSrqHSPJwTsOwWENyvMyZHNnrY8agqNq3UMtwrGU0Qy5559h3Xv/AnDUVsoWTaPlGG5HXuqP9nHCUr52zzx4dIAT/Y1e0xRshibGwYa0YAXFnY7LlEUAIiIAiIgCIiAIiIAiIgKzx02qlsLoKZ+gyO83Y+h9Mrwe4UVXSzOFVG4OJJ1HkV9J3GhjuFI6CQludwR0K87v/D1RS6mVNP4sJ5SAZaVHaZP4edcN0lvrroKa4y+FHIxwZIXYDX42z9Va62nrXWKqfe46aOe3taKOsY4B7iDsNjuPdQldwwS10tE7luWHooCpjqYnaJ9YxyDjtj0XRS6oijtV3OqrpnyVUpke8gknutdjTI8ALqRlZKWOSWdsUbC97zpaBzygNu32qpucxipIy/SMud2C6XS3f01zGvlBc78JGCF6JbqSDhexOkmIa4N1yv7nt+y8yu9xkutwfUyfM92fYdAq33RLVIwsc6N4exzmuactc04IKlG8QSVMYgu0DayNuwkdtI3/wAuZ+qigdwBzXcNL8+Xnvsrpsq0XO33amgtfgRNFypB/wBFxAmiHYg7ELUdxVaLZHUC22lzZp2lkniHSMEEEbH19OQVVBdE8OY4sc05DgcFb8N0pqhvh3am8ZuMCePyyN/Y/VXuylUaNLHPebvHE8kvmfg46D0+i9Ht/DNrrWvoo9NK6NmI9fKV3Yn9/VVO2W0U9xhuVrnbWxRPy6MeWQDrt7K+UstvqKZ5jiZUl/yOMhaYz6t5qa+ovs8/rg+Cd0cY0yRPLXb5B3Wzbbx8RA4vYY3REB7vwknkpS9PpJLnHFHSxtqDGI2xMfkyP/Mew6qCqaQVVXHZ6IF9PSEvqJWb+I/qf2C6QySjVHKeOMrsults9XcrTJc6V8bmQEh8erzAAZytigubosBzstVWNNdra0mF8kdLVRbAv2cM4x/hd7fO+Q6S/BbzBK14czk3Zi5HHjFLUur5Y5gXMIAPQKLuNV4EelvzOXXxvDEZB6brSq3GWRzj/wDxd32Y4LV0RlXVmOJxc7yt3Puqdcq01tRq30t2aFJcQ18b5fh6dxwPmOeagQN15vIy7PVeHrcfFqtn6dgFY+ELGbrcmyOYTFERkHk49AoSkpZaydkETdT3uAAXvHAHDTKGjjldGA2Mbf3O7rKzYi0WG1tttC1paBK8Zd/ClFwuVUBERAEREAREQBERAEREAREQBdXsa9pa8BzTsQRzXZEBV7xwZBVF09A8U8vPR+E/wqNeLI+Fxp7nTmInlIG5B9fVewrBU0kFZEYqiJsjDza4ZU2D52uXDlTSAyxjxoej2bj6jmFYuCbG2GH+p1DPvHHEQPQd1dL1wjBQPbU0lQY2OdgxO3/QqFu9UaWlFNAdEknlaQNm+qN/wskV7jGqmvINHRS/dwO84PJ7u2fRUOSlqKd5bNE5rh3CuUlLPb42tqI8xDcTMOWn1PUfVR15fqoXHIcMbFSopIhuyDo6aapnZBGwukkIwO+VepbLQ2OyGSpGZWty545l3Yei44EsgFKy51MZDy3EYd0HdQvHN+NfcPg4TiCA4yPxO7qrf4iyXVlcfO6ad7iAMnkEyCCN8kLC06Rk8ytqlp5KmZsETS+R5wAOpVih1hdLTSCSGVzHt5OacYUkLpBcH4u8bi9xH/ExDDx7jkf8LYuPDs1rpfFqXBvqOWeygwQ48wrKRDiWemtYt1tqa+1TC4TvGlj4h5omnmS3nlSnDNtNvtImbMwuqNpHYzpPr7KjxTVFFIJqeV8Tu7TjKsdq4rhc7Rcoyx7hg1MA83/k3k4LpFo5yTJW53qtZaY7PX0L4nxSN0T4yx46kHrnmtI0MJu+m0yPdrLRG4txkkZOfQb/AKKaNZLLbXRlwrKFxz4tKdQae7o+Y+n6LVvVwjstpM8LfDqapnhUzcYcyPq8+p/hTF12JLbo0p77RR3KSh+I/wCU7T4p2a89d+m61L5fYoIdFNK2SVwwS05ACp7g7Vk7rq45Ku+RLWjj/wA0Ntg5xe8uJyTuuWjO+F1wpvhyyvu1xbGWnwWHL/4WY1It32ccLvq5m1UrfM8eUEfK3uvb6enZTQMhjGGsGAojhiyx2m3M8gEjwM+g7KcVGSEREAREQBERAEREAREQBERAEREAREQBcOcGgknAC5UJf7j4Mfw0Z8zhlxHQIF2RV5r/AIqoLgSWM2aO63LTw7TzUj5bhA2R04+Vw+UenZa9mt3x1QJpG/dRnOO5VrAAGByQs3+FLuXBctOHSWuTWw84ZP2P8qk1PC8VZX+BUU8lK5rg6SMbNcPb+F7S5wY0uJwANyqfd674qpdIBs3ytCWEiqcRXBtst7KKm8j5BoBb/wBNvdVyooaOppmxvia4NGxxuPqrxV8H3R8RuEZbO6UeaF3MN7Duq1UWwMf4bWGllb8zHg4P06K0eg3ZRq2iipaqONpJaXY8yu/BPD4o6c3GqZh7xiMO5gd/qoqPh2pul+iinidHTxed7+jh2BVpv1wbSU7LfA7EkzceU40N7qrd9IlL9KZxjfmVte+mhw+GHygn83Uqst2apGusNTBIXMPjMPUcwteGllE+Hx/JzaVZKirdszUNufWODHEsa7YPI2BWlKwRvc0O1YJGRyK3Z6wxHwiTlw2AJwFqYyrXRX0yUNxq7bO2eknfE8Hm08/fuu9zutXd6v4msk8STAbywAPQLWLOy64VrK0cHYFYT0WV/wAq6NYXOAUMlGWkpZKiZrGMLnOOAB1Xt/2fcMMp6eOWSMaWeZx/M5UrgHhuSsqo6hzdycMGOQ6le50dLHR0rIIwA1gwubLGZcoigBERAEREAREQBERAEREAREQBERAEREBr1lSykp3Su6DYdyqm1k1yreeXPdut++zTTVYgDXFjRkFu4+vZSNmt4pYBI8edw/QIW8RvUtOylgbEwbAfqsqLXrqtlHTOldzHIdyhUjb9X6GfCxnzO+YjoFo2W3fFVHjyt+6j5A9StaKOa51nUl53PZW2mgZTQNiYMBoQs+ujJhR11s9uuULvjIWnSPnGzm/VSKguIbhpZ8LG7c7vPp2QhIqxp4bdFJ4LnyNaTpJ3J7KszUclXUPqC8iof80bxjA6Adl6BY7b8dUCplb9zGdgRzKmrlYLfc2/fQhsgG0jNnD6ov6S3+HjM7JqYPbIzBIxuMj6KqSVgguE73bsIwT6r2O68H11Mx/hMFbBjkNnj6Lzu6cLxS62UzjEdWTFISN/f+cq9lSnVdS2qnY9gIxtuFnxss01kqKKYtkYcjcDG6xluFAOMLqW5U/Z7tbIqT+n3W3Nmgc/V48e0rPbutG7wW+Crxbap1RA4ZBezSW+ikEW+MnkpOxWl1wrWxgHQN3kdlqRxukeGNGXOOAB1Xr32e8LtjYHzMyG+Z5PU9kbFFs4SsbLXb2yFuHvaMD8oVhXAAAAAxhcqgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgNZ1Gxz863ac5LM7ErYAwFyiA4JAGTyVVu1aa+q8OPJjYcN9fVWapY59O9rQCSOR6qIoLSxtUZNL9Dejxjf09EJXRtWigFJTh7h94//AUkuEJAGTyQg16+rbR0zpTz5NHcqpRQzXOt08y92XFbd3rjWVXhx7sYcD1KmbNbxR0we4feP3PooLeI3KanZTQNijGA0LMiKSoUTduHLfd2kzxBsvSRmzgpZEB5td+DKynaR4Qrqcb5x5h9P4VKruGBNqdTHLhzY7Zw+v8AK99O6iLpw1Q3Iuk0eDOeUsex+vdTf9B871VsqKV5aWE6eYxgj6LTIXsd14VqadpFXTiphbuJoxuP3CrEnCEFVUgsdmMncjZw/lSDS4K4blrqqOpczOTiMY/yvc6CijoKRkEYxpG57lRHC1lit1I2QRhp04YOwVhVQEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBcLlEAUTf640lCQw+ZwPIZKllimp4p26ZGg/sgIGyW0ySePM3ZvT1ViXWKJkMYYwYaF3QluwiIhAREQBERAcYB2UdNYrfNUtqPADJAcnRsD7hSSIDgANAAGAFyiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiA//Z

Ce module intègre des résistances pour assurer son bon fonctionnement. On peut trouver le composant sans la plaque où celui-ci est soudé, mais il faudra ajouter les composants électroniques nécessaires lors du branchement.

 

 

Connexion

De multiples articles décrivent comment faire un relevé de température avec le module DHT11. Il faut penser à le brancher sur le 5v et laisser le 3.3v pourESP8266.

L’article suivant décrit ce qu’il faut faire pour stabiliser les tensions : https://github.com/esp8266/Arduino/blob/master/doc/boards.md#improved-stability

J’ai juste ajouté un condensateur de 100µF pour éviter les reboot intempestifs du composant ESP8266 (pour cause d’instabilité du signal).

 

Code complet

Voici le code pour Arduino :

#include <SoftwareSerial.h>
#include "DHT.h"

/** DHT **/
#define DHTPIN 5
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

/** ESP8266 **/
String ssid = "NETGEAR";
String key = "xxxxxxxx";
String serverHost = "192.168.1.100";
String serverPort = "80";

SoftwareSerial esp8266(1, 0); // RX, TX
bool done = false;

void setup() {

  /** DHT **/
  dht.begin();

  /** ESP8266 **/
  esp8266.begin(115200);
  delay(500);
  esp8266.println("AT+RST");

  /**
   * Initialisation
   */
  delay(1000);
  esp8266.println("AT");
  done = esp8266.find("OK");
  if(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

  /**
   * Se mettreen mode CLIENT
   */
  esp8266.println("AT+CWMODE=1");
  done = esp8266.find("OK");
  if(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

  /**
   * Connexion auWifi
   */
  esp8266.println("AT+CWJAP=\""+ssid+"\",\""+key+"\"");
  done = esp8266.find("OK");
  while(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

  /**
   * Se mettre en mode multiple connexions
   */
  esp8266.println("AT+CIPMUX=1");
  done = esp8266.find("OK");
  if(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

  /**
   * Récupération de l'adresse IP
   */
  esp8266.println("AT+CIFSR");
  done = esp8266.find("STAIP");
  if(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

}

void loop() {
  int maxLoops = 5;

  /**
   * DHT11 : Temperature
   */

  float temperature = dht.readTemperature();
  if( isnan(temperature) ){
    return;
  }
  // convert float --> String
  String temperatureStr = "";
  char temperatureChar[15];
  dtostrf(temperature,5,2,temperatureChar);
  temperatureStr = temperatureChar;

  /**
   * HTTP GET
   */
  String cmd = "AT+CIPSTART=4,\"TCP\",\""+serverHost+"\","+serverPort;
  esp8266.println(cmd);
  delay(500);
  done = esp8266.find("OK");
  int currentLoop = 0;
  while(!done){
    delay(500);
    done = esp8266.find("OK");
    if(currentLoop >= maxLoops){
      break;
    }
    currentLoop++;
  }

  String url = "/lda/index.php/data/createdatajson/append?datflval="+
    temperatureStr +
    "&thing=FIRST";
  String cmdGET = "GET " + url + " HTTP/1.1\r\n"+
    "Host: "+serverHost+"\r\nUser-Agent: ESP8266_HTTP_Client\r\nConnection: close\r\n\r\n";
  esp8266.print("AT+CIPSEND=4,");
  esp8266.println(cmdGET.length());
  delay(1000);
  done = esp8266.find(">");
  currentLoop = 0;
  while(!done){
    delay(500);
    done = esp8266.find(">");
    if(currentLoop >= maxLoops){
      break;
    }
    currentLoop++;
  }
  esp8266.println(cmdGET + "\r\n\r\n");
  delay(1000);

  esp8266.println("AT+CIPSTATUS");
  delay(1000);

  // Fermeture de toutes les connexions
  esp8266.println("AT+CIPCLOSE=5");
  delay(1000);

  // repart à zero
  esp8266.println("AT");

  // 4 secondes sont déjà passées
  delay(16000);

}

 

 

Annexe

Photos du projet avec la platine d’essai :

 

Arduino + ESP8266 : connexion Wifi

Introduction

La carte Arduino peut être connectée à un module de connexion Wifi (nommé ESP8266) pour lequel une configuration est nécessaire.

Voici la façon dont j’ai pu envoyer un simple HTTP GET depuis la carte Arduino avec ce module.

Dans certains tutoriels qu’on peut trouver sur le Net, il est mentionné la mise à jour du firmware, par l’intermédiaire d’une autre carte électronique. Cet article part du module sorti de son emballage, sans faire cette mise à jour.

 

Le module ESP8266

Ce module est un regroupement de multiples composants, sous forme de mini-carte électronique, avec 8 broches de connexion.
Il mesure 2.5 cm x 1.5 cm et coûte 7.33 € sur Amazon.

Il permet de se connecter à un point d’accès Wifi pour communiquer en mode client/serveur (être client ou être serveur) mais il peut aussi être défini en tant que point d’accès Wifi.

Branchements

Il faut brancher le composant avec la carte Arduino, de manière précise, sur le 3.3v.

 

Détail des branchements :

L’article http://www.labradoc.com/i/follower/p/notes-esp8266 détaille les branchements. En voici un rappel (en Français) :

  • RX vers le port TX de la carte Arduino
  • VCC vers le port 3.3v de la carte Arduino
  • GPIO 0 non branché
  • RESET non branché
  • CH_PD vers le port 3.3v de la carte Arduino
  • GPIO 2 non branché
  • TX vers le port RX de la carte Arduino
  • GND vers le port GND de la carte Arduino

Si vous avez branché RX et TX correctement, une LED bleue sur le module ESP8266 émet un bref flash lors de la mise sous tension. Si elle ne s’allume pas, c’est que le branchement n’est peut-être pas bon…

Quelques conseils :

  • Branchez tout et à la fin, le fil qui va vers 3.3v. Vous saurez si tout est opérationnel ou non avec cette LED bleue.
  • Il ne faut rien brancher avec le port 5v de la carte Arduino. En cas d’erreur, le module grillera. Je vous conseille donc de brancher un fil sur ce port 5v et de laisser non branché à l’autre bout du fil (par exemple, faire un nœud pas trop serré).

 

Comment donner des ordres au module ESP8266 ?

L’article http://www.labradoc.com/i/follower/p/notes-esp8266 détaille les commandes à passer par l’interface de programmation d’Arduino. Pour résumé, une série de commandes doivent être passées pour :

  • Initialiser le module ESP8266
  • Se mettre en mode client Wifi (et pas en mode point d’accès)
  • (en option) Rechercher les points d’accès disponibles (et afficher leur SSID)
  • (en option) Se définir une adresse IP
  • Se connecter sur un point d’accès (avec SSID et mot de passe)
  • Se mettre en mode « Connexions multiples » (pour faire plusieurs appels HTTP)
  • (en option) Afficher son adresse IP
  • (en option) Faire un PING sur un serveur
  • Faire un HTTP GET
  • Fermer la connexion (et préparer le prochain appel HTTP)

Ces commandes sont à transmettre par le port Série de la carte Arduino.

 

Code pour faire les appels HTTP GET

Avec Adruino, 2 fonctions sont préparées pour que l’utilisateur écrive son code à l’intérieur, avec 2 objectifs :

  • setup() : cette fonction est lancée une seule fois au démarrage de la carte Arduino
  • loop() : après la fonction « setup() », cette fonction est lancée autant de fois que la carte Arduino est allumée

Une partie d’initialisation est faire avant ces 2 fonctions

#include <SoftwareSerial.h>

String ssid = "NETGEAR";
String key = "pA$$W0Rd";

SoftwareSerial esp8266(1, 0); // RX, TX
bool done = false;

Seule la bibliothèque SoftwareSerial est nécessaire.

La variable « done » sera utilisée de manière globale.

 

Code de la fonction setup()

 void setup() {
  // put your setup code here, to run once:

  esp8266.begin(115200);
  delay(500);
  esp8266.println("AT+RST");

  /**
   * Initialisation
   */
  delay(1000);
  esp8266.println("AT");
  done = esp8266.find("OK");
  if(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

  /**
   * Se mettre en mode CLIENT
   */
  esp8266.println("AT+CWMODE=1");
  done = esp8266.find("OK");
  if(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

  /**
   * Affecter son adresse IP manuellement
   */
   /*
  delay(1000);
  esp8266.println("AT+CIPSTA=\"192.168.1.200\",\"192.168.92.254\",\"255.255.255.0\"");
  done = esp8266.find("OK");
  while(!done){
    delay(1000);
    done = esp8266.find("OK");
  }*/

  /**
   * Rechercher les points d'accès WIFI
   */
   /*
  delay(1000);
  esp8266.println("AT+CWLAP");
  done = esp8266.find("OK");
  while(!done){
    delay(1000);
    done = esp8266.find("OK");
    delay(3000);
    break;
  }*/

  /**
   * Se connecter au point d'accès Wifi défini dans la variable "ssid"
   */
  esp8266.println("AT+CWJAP=\""+ssid+"\",\""+key+"\"");
  done = esp8266.find("OK");
  while(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

  /**
   * Se mettre en mode connexions multiples
   */
  esp8266.println("AT+CIPMUX=1");
  done = esp8266.find("OK");
  if(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

  /**
   * afficher son adresse IP
   */
  esp8266.println("AT+CIFSR");
  done = esp8266.find("STAIP");
  if(!done){
    delay(1000);
    done = esp8266.find("OK");
  }

  /**
   * faire un ping sur un server
   */
   /*
  delay(1000);
  esp8266.println("AT+PING=\"192.168.1.100\"");
  done = false;
  if(!done){
    delay(1000);
    done = esp8266.find("OK");
  }*/

}

Il est nécessaire de mettre des temps d’attente (moins une seconde maxi) pour que le module se mette dans un état convenable, qu’il communique avec le point d’accès Wifi. Sinon, vous aurez des messages du genre « busy p… » ou « busy s… », qui peuvent être bloquant ou non.

Ajustez ces temps selon vos résultats.

 

Code de la fonction loop()

void loop() {
  int maxLoops = 5;

  /**
   * Faire un HTTP GET
   */
  String cmd = "AT+CIPSTART=4,\"TCP\",\"192.168.1.100\",80";
  esp8266.println(cmd);
  delay(500);
  done = esp8266.find("OK");
  int currentLoop = 0;
  while(!done){
    delay(500);
    done = esp8266.find("OK");
    if(currentLoop >= maxLoops){
      break;
    }
    currentLoop++;
  }

  String url = "/lda/index.php/data/createdatajson/append?datflval=1.55&thing=FIRST";
  String cmdGET = "GET " + url + " HTTP/1.1\r\n"+
    "Host: 192.168.1.100\r\nUser-Agent: ESP8266_HTTP_Client\r\nConnection: close\r\n\r\n";
  esp8266.print("AT+CIPSEND=4,");
  esp8266.println(cmdGET.length());
  delay(1000);
  done = esp8266.find(">");
  currentLoop = 0;
  while(!done){
    delay(500);
    done = esp8266.find(">");
    if(currentLoop >= maxLoops){
      break;
    }
    currentLoop++;
  }
  esp8266.println(cmdGET+"\r\n\r\n");
  delay(1000);

  esp8266.println("AT+CIPSTATUS");
  delay(1000);

  // Close all connections
  esp8266.println("AT+CIPCLOSE=5");
  delay(1000);

  // restart from zero
  esp8266.println("AT");

  // 4 secondes déjà passées
  delay(20000);

}

 

Je désire faire l’appel à l’URL « http://192.168.1.100:80/lda/index.php/data/createdatajson/append?datflval=1.55&thing=FIRST » en GET. Tous les paramètres de cette requête sont après le caractère « ? ».

J’ai un serveur HTTP qui est capable de lire cette requête et de stocker la valeur « 1.55″ pour un objet « FIRST » dans une base de données.

Ce code se découpe en plusieurs parties :

  1. Se connecter au serveur 192.168.1.100, en mode TCP, sur le port 80 : « AT+CIPSTART=… ». Il faut attendre que le moduleESP8266 réponde « OK » pour continuer.
  2. Appeler l’URL en GET : « AT+CIPSEND=… ». Il faut absolument ajouter dans l’entête de cette requête le « Host: xxx », sinon le serveur répond « Error HTTP 400 : BAD REQUEST ». Pour être propre, je rajoute « User-Agent » et « Connection ». Les « \r\n » sont importants. A la fin, il faut les mettre en double : « \r\n\r\n ». Sinon, le serveur attend des données.
  3. (En option) Demander le statut de la connexion : « AT+CIPSTATUS ». elle doit être fermée (le serveur a pris la requête est il a fermé la connexion)
  4. S’assurer que toutes les connexions sont bien fermées : « AT+CIPCLOSE=5″ (le chiffre 5 signifie ‘toutes les connexions’)
  5. Réinitialiser le module ESP8266 : « AT ». Lors de la prochaine connexion au serveur (étape 1 de la boucle), la connexion Wifi sera conservée.
  6. Attendre 20 secondes pour le prochain envoi de données

Ici aussi, les délais d’attente entre les commandes sont importants. Ils peuvent être personnalisés selon votre besoin, mais ils ne pourront pas être mis à 0.

 

Envoi vers la carte Arduino

Les ports TX et RX sont utilisés lors du transfert du programme vers la carte Arduino. si vous laisser le 3.3v branché, Il faudra débrancher les TX et RX pour que le transfert se fasse correctement. Je réitère donc mon conseil de débrancher le 3.3v lors du transfert.

 

Résultat sur le serveur

Les données sont toutes récupérées, à intervalle régulier (toutes les 24 secondes) :

 

Trace laissée par le module

Dans la console « Moniteur série », voici les infos affichées par le module. On y trouve les commandes passées et les réponses du module.

Ai-Thinker Technology Co.,Ltd.

ready
AT

OK
AT+CWMODE=1

OK
AT+CWJAP="NETGEAR","pA$$W0Rd"

WIFI CONNECTED
WIFI GOT IP

OK
AT+CIPMUX=1

OK
AT+CIFSR

+CIFSR:STAIP,"192.168.1.200"
+CIFSR:STAMAC,"5c:cf:7f:10:d6:ab"

OK
AT+CIPSTART=4,"TCP","192.168.1.100",80

4,CONNECT

OK
AT+CIPSEND=4,157

OK
> 

busy s...

Recv 157 bytes

SEND OK

+IPD,4,728:HTTP/1.1 200 OK
Date: Sat, 16 Jan 2016 17:32:03 GMT
Server: Apache/2.4.12 (Ubuntu)
Set-Cookie: ci_session=a%3A5%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%227c75547487eb7803fa0afe75dead178d%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A13%3A%22192.168.1.200%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A19%3A%22ESP8266_HTTP_Client%22%3Bs%3A13%3A%22last_activity%22%3Bi%3A1452965523%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7Deb938672b48c15066e92aef755ab6250; expires=Sat, 16-Jan-2016 19:32:03 GMT; Max-Age=7200; path=/
Access-Control-Allow-Origin: *
Vary: Accept-Encoding
Content-Length: 81
Connection: close
Content-Type: text/html; charset=UTF-8

{"datiddat":41,"datflval":"1.55","datdhacq":"2016-01-16 18:32:03","datidthn":"1"}4,CLOSED
AT+CIPSTATUS

STATUS:4

OK
AT+CIPCLOSE=5

OK
AT

OK
AT+CIPSTART=4,"TCP","192.168.1.100",80

4,CONNECT

OK
AT+CIPSEND=4,157

OK
> 

busy s...

Recv 157 bytes

SEND OK

+IPD,4,728:HTTP/1.1 200 OK
Date: Sat, 16 Jan 2016 17:32:27 GMT
Server: Apache/2.4.12 (Ubuntu)
Set-Cookie: ci_session=a%3A5%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%220a10f1ca33ba3316e6acfee18ba76b2f%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A13%3A%22192.168.1.200%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A19%3A%22ESP8266_HTTP_Client%22%3Bs%3A13%3A%22last_activity%22%3Bi%3A1452965547%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7Db946991f8b9d1ef88d0667330b515767; expires=Sat, 16-Jan-2016 19:32:27 GMT; Max-Age=7200; path=/
Access-Control-Allow-Origin: *
Vary: Accept-Encoding
Content-Length: 81
Connection: close
Content-Type: text/html; charset=UTF-8

{"datiddat":42,"datflval":"1.55","datdhacq":"2016-01-16 18:32:27","datidthn":"1"}4,CLOSED
AT+CIPSTATUS

STATUS:4

OK
AT+CIPCLOSE=5

OK
AT

OK
...

 

Annexe

Toutes les commandes et la doc technique (en anglais) de ce module ESP8266 : https://nurdspace.nl/ESP8266

Code complet : téléchargez

 

Raspberry : un feu de cheminée ou un aquarium

Introduction

Noël approche, qui n’a pas rêvé d’avoir un feu de cheminée. Je vous propose de transformer votre Raspberry en « boite à diffuser une vidéo de distraction », comme un feu de cheminée, un aquarium ou une jolie vue de ruisseau.

 

Configuration du Raspberry

Rien de plus qu’un Raspberry qui fonctionne et qui est connecté à un écran.

 

Choix de la vidéo

Youtube fourni un grand nombre de vidéos, en HD de feu de cheminée ou d’aquarium. Voici mon choix :

https://www.youtube.com/watch?v=0fYL_qiDYf0

La vidéo dure 2 heures, mais on n’a besoin de tout le fichier. Il faut passer par une outil de téléchargement (cherchez « youtube downloader firefox plugin ») pour récupérer le fichier MP4.

 

Extraction d’une portion de quelques minutes

Passez par ffmpeg pour extraire une partie de la vidéo. Notez la période : début et durée, puis lancer la ligne de commande suivante pour l’extraction :

ffmpeg -ss 00:30:00 -i feuDeCheminee-full.mp4 -t 00:02:00 -vcodec copy -acodec copy feu-portion.mp4

Sur la vidéo « feuDeCheminee-full.mp4 », je saute 30 minutes du début et je prends 2 minutes de vidéo finale. Le fichier « feu-portion.mp4 » contiendra le résultat : une vidéo de quelques dizaines de Mo (au lieu de plus d’un Go pour la vidéo complète).

 

Lire la vidéo avec le Raspberry

Tentez de lire la vidéo avec VLC : c’est très lent. Impossible de choisir cette solution. En allant voir sur le Net, le player conseillé est « omxplayer ». Cherchez à l’installer avec les bibliothèques pour être sûr du résultat.

Mon Raspberry est configuré en point d’accès Wifi, avec une application Web pour le piloter (depuis mon smartphone ou depuis un PC). J’ajoute cette fonctionnalité, en appelant un script pour lancer la vidéo :

#!/bin/bash

export DISPLAY=:0.0

SERVICE='omxplayer'
RES=0
trap "exit" INT
while [ $RES -eq 0 ]; do
  if ps ax | grep -v grep | grep $SERVICE > /dev/null
  then
    sleep 1
  else
    xterm -fullscreen -fg black -bg black -e omxplayer --win "0 0 1600 1200" -r $1
    RES=$?
    echo "RES: $RES"
  fi
done

xrefresh -display :0

 

Vous constaterez des lignes particulières (que j’utilise peu) :

trap "exit" INT

Cela signifie que si un sous-script est lancé et qu’il se fait killer, le script s’arrête aussi.

 

xrefresh -display :0

A la fin du script, je force le rafraîchissement de l’écran pour nettoyer ce qui peut rester.

 

xterm -fullscreen -fg black -bg black -e omxplayer --win "0 0 1600 1200" -r $1
RES=$?

La ligne de commande principale, qui lance la vidéo, dans un terminal (xterm), en plein écran, avec omxplayer en mode fenêtre, d’une taille de 1600×1200. La variable RES récupère le code retour du player. Si elle vaut 0, on boucle, sinon, on s’arrête (le CTRL-C fait un RES = 2).

Pour simuler la lecture sans fin, j’ai une boucle while autour de tout ça.

 

Arrêt de la lecture par un script

J’ai aussi un autre script qui arrête la lecture en boucle :

PID=$(ps aux | grep omxplayer | awk {'print $2'})
kill -9 $PID 2>/dev/null

xrefresh -display :0

Rien de très novateur. J’ai essayé la commande « killall -9 omxplayer », mais elle n’est pas satisfaisante.

Mon interface (depuis un PC) pour lancer quelques vidéos et un bouton en bas pour arrêter :

Arduino : premiers essais

Introduction

Après avoir complètement paramétré mon Raspberry Pi en point d’accès wifi, avec une webcam et une application web pour le piloter depuis mon smartphone (article complet), je voulais essayer jouer avec les PIN du GPIO. Mais je ne voulais pas refaire un autre Raspberry pour jouer avec les « sensors » (détecteur de température, détecteur de mouvement, etc.). Pour faire ça, il y a Arduino, en plus simple et plus économique…

 

 

Installation de l’environnement

Depuis longtemps, je suis habitué à l’IDE Eclipse pour mes développements. J’ai pris mes réflexes et je pense être moins désorienté dans ces environnement. C’est pour cette raison que j’installe un Eclipse dédié.

J’ai donc suivi quelques tutoriels, mais celui-ci me semble très complet pour tout savoir sur l’interface entre ma machine et la carte Adruino : http://www.codeproject.com/Articles/1003347/Creating-Arduino-programs-in-Eclipse

Avec Ubuntu, le port est à changer. Pour savoir quel port il faut configurer (« //./COM15″ dans le tutoriel), voici les étapes, depuis un terminal :

  1. Branchez votre carte Arduino sur le port USB et tapez « lsusb ». Si une ligne apparaît avec « Arduino », c’est bon, la carte est reconnue.
  2. Tapez « dmesg ». Vous devriez voir :
    [ 1013.247248] usb 8-2: Product: Arduino Uno
    [ 1013.247250] usb 8-2: Manufacturer: Arduino Srl
    [ 1013.247253] usb 8-2: SerialNumber: 85431303636351D0A101
    [ 1013.272419] cdc_acm 8-2:1.0: ttyACM0: USB ACM device
  3. Tapez « ls /dev/ttyACM* ». Vous devriez avoir le port de l’Arduino : /dev/ttyACM0
  4. Il reste à permettre à l’utilisateur d’écrire dans ce device : sudo chmod 777 /dev/ttyACM0

 

Premier script : « blink »

En suivant le tutoriel, je crée le programme « blink.cpp » en copiant / collant le code fourni, mais une erreur survient sur l’include :

#include "arduino.h"

 

Il faut la changer en :

#include "Arduino.h"

En consultant le fichier recherché dans mon code (version 1.6.6 d’Arduino), il y a un « A » (majuscule).

Le build se passe correctement et l’installation sur la carte aussi. La LED clignote bien.

 

Conclusion

Même si l’IDE fourni par Arduino fonctionne très bien, il m’a fallu un peu plus d’une heure pour installer quelques plugins et adapter le dernier. J’espère que cet article fera gagner du temps à ceux qui essaieront.