Ça y est ! La partie tant attendue débute enfin ! Le développement de l’application client qui me permet de contrôler le PiRobot à distance via une tablette sous Windows 10 ! Bon, l’objectif ici est de créer une interface simple, permettant de discuter avec le PiRobot et de piloter les moteurs à distance via le réseau ! C’est parti ! 🙂

 

 

 

1. Rappel : structure de notre réseau

 

Ce chapitre est un petit rappel sur la structure de mon réseau. La communication avec le PiRobot (contrôle via l’interface client ou serveur) ou la programmation du PiRobot (avec Visual Studio 2017 RC) passe par mon réseau local. L’objectif final étant, bien entendu, de se passer de réseau local.

 

1.1. Réseau en mode de développement

Voici schéma réseau qui représente ma configuration actuelle. Elle me permet de programmer, débuguer sur PC de DEV et de tester mon application client directement sur tablette. Chaque élément possède une IP fixe sur le réseau.

Configuration de développement en « mode programmeur »

Configuration de développement en « mode programmeur »

 

1.2. Vision finale : Piloter directement le PiRobot depuis la tablette sans réseau local !

Lorsque le développement sera achevé (côté client, comme côté serveur), l’idée est de me connecter directement avec la tablette sur le Raspberry Pi du PiRobot. On créera donc un pont Wifi entre les deux éléments. On appelle aussi ça le Wifi Direct ;-).

Plus besoin de réseau local ! Ouf ! :-)

Plus besoin de réseau local ! Ouf ! 🙂

 

2. Structure du programme client

Je développe l’application client en C#/WPF afin de profiter des technologies .NET. Cela me permet également de me perfectionner dans ces technologies :-).

Dans une application WPF, deux parties sont bien distinguées : l’interface IHM (thread UI) et le code behind (main thread). C’est dans le code behind que je bind (comprendre « relier ») mes valeurs de l’interface afin de pouvoir mettre à jour les valeurs systèmes ou les valeurs de la vue selon des événements ou des changements de propriétés. Le code behind est intimement lié à l’interface. Si un code trop long y est exécuté, ou des tâches trop lourdes, l’interface perds de sa fluidité et peut connaître de nombreux freeze. L’exécution d’un client UDP tournant continuellement y est impossible sans freezer l’interface.

Afin de palier à ce problème, je crée une classe « UDP » qui est un singleton, c’est-à-dire une classe unique qui ne sera instanciée qu’une fois et qui sera accessible à tout endroit de mon programme, ce qui est relativement pratique pour une interface de communication. C’est dans cette classe UDP que je lance mon thread client qui va sans cesse envoyer les commandes au PiRobot via le protocole UDP. Il écoutera également la réponse du PiRobot qui lui renverra des informations sur l’état du robot (voir article précédent sur le PiRobot – Partie 4).

Structure de mon programme

Structure de mon programme

Cette structure n’est pas lourde. De plus, notre programme possède une vue principale composée de plusieurs pages. Chaque page est associée à sa classe. Depuis chacune de ces classes, on pourra accéder au Singleton « UDP ». Cela me permettra d’être un minimum évolutif, sans trop me compliquer la tâche avec une architecture compliquée.

3. Une classe UDP pour les gouverner tous !

Ce singleton est la clé de voûte de mon programme. L’ensemble du code commenté est disponible ci-dessous :

Classe UDP
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Net.Sockets;
using System.Net;

namespace Client
{
    public class UDP
    {

        //-----------
        // VARIABLES
        //-----------

        // L’instance du singleton. Cette instance doit être privée et statique.
        private static UDP instance = null;

        // Pour éviter, lors de l’utilisation de multiple thread, que plusieurs singleton soit instanciés.
        private static readonly object myLock = new object();

        //Paramètres de connexion
        public static string IP { get; private set; }
        public static string Port { get; private set; }

        //Variables à passer pour la trame (set via les sliders de l'interface !)
        public static string Pwm1 { get; set; }
        public static string Pwm2 { get; set; }
        public static string Pwm3 { get; set; }
        public static string Pwm4 { get; set; }

        // Déclaration du thread Client/Serveur UDP
        static Thread myThread;

        //Variable d'état du serveur
        public static bool Connecte { get; private set; }


        //----------
        // METHODES
        //----------

        // Propriété pour récupérer l'instance du singleton
        public static UDP Instance()
        {
            return instance;
        }

        // Le constructeur d’un singleton est TOUJOURS privé.
        private UDP(string paramIP, string paramPort)
        {
            IP = paramIP;
            Port = paramPort;
            Connecte = false;
            Pwm1 = Pwm2 = Pwm3 = Pwm4 = "0";    //Par sécurité, la vitesse des moteurs est initialisée à 0.
        }

        public static void CreateConnexion(string paramIP, string paramPort)
        {
            lock (myLock)
            {
                if (instance != null) throw new InvalidOperationException("Le singleton est déjà instancié !");
                instance = new UDP(paramIP, paramPort);
            }
        }


        public static void ConnectUDP()
        {

            // Instanciation du thread, on spécifie dans le 
            // délégué ThreadStart le nom de la méthode qui
            // sera exécutée lorsque l'on appele la méthode
            // Start() de notre thread.
            myThread = new Thread(new ThreadStart(ThreadLoop));

            // Lancement du thread
            myThread.Start();
        }

        // Cette méthode est appelé lors du lancement du thread
        // C'est ici qu'il faudra faire notre travail.
        public static void ThreadLoop()
        {

            int i = 0;

            // Tant que le thread n'est pas tué, on travaille
            while (Thread.CurrentThread.IsAlive)
            {
                i++;
                // Attente de 500 ms
                Thread.Sleep(500);

                // Affichage dans la console
                Console.WriteLine("Connexion en cours...");

                var client = new UdpClient();
                client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                IPEndPoint epClient = new IPEndPoint(IPAddress.Parse(IP), Convert.ToInt32(Port)); // endpoint where server is listening


                try
                {
                    client.Connect(epClient);
                    string nb_connect = "nombre d'essais de connexions : " + i;
                    byte[] send_buffer = Encoding.ASCII.GetBytes(nb_connect);
                    client.Send(send_buffer, send_buffer.Length);
                    client.Client.ReceiveTimeout = 5000;

                    var receivedData = client.Receive(ref epClient); //puis recois un retour du serveur

                    if (receivedData != null)
                    {
                         Console.WriteLine("Données reçues de " + epClient.ToString()); //lecture de retour serveur
                         string retour = Encoding.ASCII.GetString(receivedData);
                         Console.WriteLine("Client UDP connecté ! " + retour);
                         Connecte = true;
                    }

                }
                catch(Exception e)
                {
                    Console.WriteLine(e);
                    Console.WriteLine("Connection FAIL");
                    Connecte = false;
                }

                while (Connecte == true && Thread.CurrentThread.IsAlive)
                {
                    Thread.Sleep(10);
                    byte[] send_buffer = Encoding.ASCII.GetBytes(Pwm1 + ";" + Pwm2 + ";" + Pwm3 + ";" + Pwm4 + ";");
                    client.Send(send_buffer, send_buffer.Length);
                    var receivedData = client.Receive(ref epClient);
                    string retour = Encoding.ASCII.GetString(receivedData);
                    Console.WriteLine(retour);
                }

            }
        }

        //Méthode permettant de détruire le thread et de clore la connexion UDP
        public static void CloseUDPConnexion()
        {
            myThread.Abort();
        }

    }
}

Enfin, pour instancier la classe UDP et créer une connexion, j’appelle les fonctions suivantes dans le main thread :

Création UDP et connection !
            UDP.Instance();
            UDP.CreateConnexion("192.168.0.23", "26000");
            UDP.ConnectUDP();

4. Réalisation d’une interface un peu (très !) sommaire…

4.1. Structure de l’interface

L’interface graphique est composée d’une fenêtre principale (MainWindow) ainsi que de plusieurs pages. Les pages sont affichées dans la Frame de la MainWindow. Je trouve cette manière de départager l’interface très intéressante, car cela permet de départager chaque classe de manière propre. De plus, au changement de page, la frame est réinitialisée et mis à jour automatiquement, ce qui n’est pas forcément le cas avec des onglets en WPF (TabControl). Je trouve ce fonctionnement plus souple pour le développement, et le code est mieux réparti.

Petit schéma explicatif de l’interface :-)

Petit schéma explicatif de l’interface 🙂

Je développe l’interface graphique au fur et à mesure des fonctionnalités que j’ajoute au PiRobot. Pour cet article, je me suis focalisé sur la connexion UDP au PiRobot et au contrôle des moteurs côté gauche et droite.

Les personnes qui ont lu cet article ont aussi lu :  [PiRobot] Partie 2 – Installation de WiringPi et configuration des bibliothèques I²C

4.2. Création de la page “Connexion” pour se… connecter ! Et heu… pas que !

Cette page fera office de page de connexion et débogage. Ça sera un peu ma page « bas niveau » où je vais pouvoir renseigner l’IP du PiRobot, le port UDP, la future configuration pour le streaming vidéo et le type de contrôle utilisé. Pour le moment, je n’ai ajouté que les deux premiers points de cette liste.

Une zone de texte est prévue pour afficher les informations transmises dans la zone « Retour / Debug ». A l’avenir, j’y afficherai le contenu pertinent de la console de Debug. Pour se faire, il faudra que j’intègre un logger dans l’application.

La page de « Connexion ». Très sommaire pour le moment.

La page de « Connexion ». Très sommaire pour le moment.

4.3. Page pilotage [En cours de développement]

Cette page est en cours de développement. Elle m’a permis de valider le contrôle des moteurs gauches et droites via deux sliders sur l’interface. Un pour les moteurs gauche et l’autre pour le côté droit. Je les ai paramétrés entre les valeurs -4096 et +4096 avec 0 comme valeur par défaut. Cela me permet de contrôler les moteurs du PiRobot sur toute leur amplitude. J’ai également ajouté un bouton « Zero » pour chaque paire de moteur afin de réaliser le retour à zéro (sinon, faut être ultra précis avec les sliders sur une amplitude totale de 8192 points !)

C’est le mode de pilotage le plus simple à développer et à appréhender que j’ai trouvé pour piloter aisément le PiRobot depuis l’interface tactile. J’avais déjà développé un stick analogique virtuel dans un développement antérieur, et le contrôle de la vitesse était difficile à prendre en main.

Alors là, on est sur du très très très sommaire… On est en phase de test, hein ? :-)

Alors là, on est sur du très très très sommaire… On est en phase de test, hein ? 🙂

 

Les personnes qui ont lu cet article ont aussi lu :  [PiRobot] Partie 4 – Refactoring du code PiRobot et réflexions sur le pilotage

5. Problèmes et difficultés rencontrés

Cette phase de développement est relativement simple à mettre en place. Il est rapide et facile de monter une interface client/serveur UDP en .NET avec le langage C#. Ayant déjà quelques familiarités avec le WPF, j’ai également pu réaliser une interface « convenable », même si le design est encore très loin de me plaire !

J’ai tout de même rencontré 3 difficultés/problèmes pendant cette phase de développement :

  • Je viens tout juste de relire mon article sur la Partie 4 du PiRobot. Et effectivement, déjà à ce stade de développement (oui, je parle bien du développement serveur !), un problème apparaît au grand jour sur Packet Sender ! (Je ne l’avais pas remarqué à l’époque !). En effet, lorsque j’écrivais sur l’adresse IP statique de l’interface Wifi du PiRobot, c’est-à-dire 192.168.0.23 :26000, ce dernier me répondait depuis l’adresse IP 192.168.0.27 :26000. Lorsque je développais sur la partie client, je ne comprenais pas comment j’arrivais à lui envoyer des trames, mais pas en recevoir de la part du PiRobot. Le phénomène était exactement le même qu’avec Packet Sender. Le PiRobot me répondait depuis l’interface Wifi, mais avec une adresse IP différente !

    • Explications & Solution : sur le Raspberry Pi, dans le fichier /etc/network/interface, l’interface wifi (wlan0) est correctement configurée en IP statique et cette IP réponds parfaitement au ping et permet même la connexion SSH. Cependant, le client DHCP du Raspberry Pi est toujours actif. Et même si l’interface IP statique est préférée, le client DHCP fait une demande d’attribution d’IP au serveur DHCP (dans mon cas, ma Freebox) au démarrage du Raspberry Pi. L’interface Wifi possède donc 2 IP dont l’IP statique est « censée » être prioritaire sur l’autre (c’est là que le bât blesse). Si je supprime cette IP de l’interface avec la commande sudo ip addr del 192.168.0.27 dev wlan0, tout rentre dans l’ordre. Cependant je dois rentrer cette commande à chaque redémarrage. Afin de ne plus avoir le problème, je dois reconfigurer définitivement le client DHCP du Raspberry Pi en modifiant le fichier /etc/dhcpdc.conf comme indiqué dans le poste suivant : http://raspberrypi.stackexchange.com/questions/52010/set-static-ip-and-stop-dhcp-on-jessie-lite

  • Après redémarrage de Raspbian sur le PiRobot, lorsque je lance le serveur sur le PiRobot sans passer par Visual Studio 2017, je n’arrive plus à commander le sens des moteurs.

    • Explications & Solutions : Quand je lance le serveur à partir de Visual Studio, je lance une commande pré-compilation / post lancement du serveur : gpio export 4 out; gpio export 17 out; gpio export 27 out; gpio export 22 out;. Ces commandes paramètrent les pins du GPIO que j’utilise en mode OUTPUT. Cela doit être fait à chaque démarrage du Raspberry Pi. Deux solutions : soit je lance un script au démarrage pour le faire (et qui plus es pourrait également lancer l’application Serveur), soit je trouve le fichier de configuration des modes GPIO pour le paramétrer indéfiniment.

  • Je n’arrive pas à utiliser les deux sliders en même temps sur la tablette en multi-touch.

    • Explications & Solutions : Les événements tactiles sont supportés depuis la version 4 de WPF avec les TouchEvents. Cependant après m’être renseigné, les TouchEvents supportent nativement le multi-touch lorsque plusieurs points sont reconnus sur un objet (possibilités de « pinch to zoom » ou « pinch to resize » sur des formes par exemple). Par contre, il n’est pas possible nativement d’utiliser plusieurs contrôle UI (ElementUI) en parallèle grâce au multitouch. Plusieurs solutions s’offrent à moi : utiliser des pirouettes techniques permettant de « détacher » certains éléments de l’UI et de faire des threads de surveillance tactile sur ces objets là à part du Thread UI. Soit utiliser le framework Microsoft Surface SDK 2.0 qui propose des « SurfaceSliders » qui seraient compatible multitouch avec une gestion multi-composants inclus dans le SDK. Une dernière possibilité serait de passer l’application en UWP (Universal Windows Plateform) où le tactile multipoint serait mieux géré. Après, quelques exemples d’utilisation d’interfaces multi-touch en WPF existent sur le net. C’est encore à étudier !

Et je précise : ma tablette est multi-touch ! 🙂

6. Conclusion et poursuite du projet !

Dans son ensemble cette partie du développement était plutôt simple. Dommage que je sois resté bloqué assez longtemps sur cette interface à 2 IP ! Le pattern Singleton simplifie grandement l’application client et fait de ma classe UDP le noyau principal de ma petite application client. De plus, avec une trame envoyée toutes les 10 ms, l’interface ne souffre pas de ralentissement ! Je suis impressionné par la rapidité des échanges de données ! Entre les résultats que j’ai eus il y a un peu plus d’un an en programmant ce même type d’application en Python, il n’y a pas photo ! Je ne regrette pas mon choix d’être passé sur des langages compilés plutôt qu’interprété ! Aucune latence se fait sentir entre le moment où je bouge le slider et où les moteurs réagissent. Un véritable régal temps réel !

Le prochain objectif est de gérer le multi-touch sur plusieurs éléments de l’interface !

Lorsque j’aurai résolu le problème du multi-touch, je sortirai une petite vidéo de démonstration.

 

BenTeK.

 

Pin It on Pinterest

Shares
Share This