ex0ns

about
A Computer Science Student's blog, about programming, algorithms, hack and a lot more.

GUI en C++ avec GTKmm !

15 Jan 2013

Je tiens toujours mes promesses, bien que peu respectueux des délais, mais voici la suite de l'article sur la gestion des processus sur linux, dans lequel je vous dois une explication sur GTKmm et son fonctionnement (je ne suis personellement pas très fan des GUI, mais dans notre cas, elles peuvent s'avêrer utiles).

Qu'est ce que GTKmm ?

gtkmm is the official C++ interface for the popular GUI library GTK+

Pour les anglophobes, il s'agit d'une "interface" en C++ afin de manipuler la bibliothèque GTK+ (tout comme pyGTK est une interface python pour cette même bibliothèque). Ainsi, nous alons pouvoir créer des interfaces graphique en C++ tout comme en C, en utilisant tous les bénéfices qu'apporte ce langage !

Un tutoriel d'introduction est proposé sur le site de gnome il peut être interessant d'y jetter un coup d'oeil avant de lire cet article (mais nous n'aborderons que des aspects très simples de la bibliothèque). Une autre page, à laquelle je fairais réference de nombreuses fois dans l'article est la documentation, elle est très bien faite, très complète, alors pourquoi s'en priver ?!

Pour en faire quoi ?

Si vous avez lu mon article sur la gestion des processus (ce que j'espère que vous avez fait étant donné qu'il s'agit de la suite), vous aurez probablement compris que nous allons créer une interface graphique (GUI) nous permettant de visionner les processus actifs, qui les "possède", et la possibilité de les arrêter, à la fin de cet article notre interface ressemblera à quelque chose comme : final

Cette dernière est très sommaire, mais libre à vous d'y rajouter ce que vous voulez, le but de cet article est d'introduire GTKmm afin de vous montrer sa polyvalence (et sa simplicité). Comme d'habitude, le projet final sera disponible en téléchargement ici.

Let's go !

Avec quoi ?

Pour suivre cet article, et pouvoir compiler vos projets, vous autre besoin d'installer GTKmm (version 3.0) (disponible dans les dépots officiels), sur Fedora, un petit:

yum install gtkmm30-devel

Pour compiler nos projets, nous utiliserons la commande (comme indiqué sur la documentation):

g++ program.cpp -o program `pkg-config --cflags --libs gtkmm-3.0`

Il ne faut pas oublier d'inclure le header #include <gtkmm.h> (ou ses dérivés) dans notre fichier source !

Afin que nous partions tous sur les mêmes bases, voici le code source sur lequel nous partirons :

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <map>
#include <sys/types.h>
#include <dirent.h>
#include <pwd.h>

int strToInt(std::string value){
    std::istringstream temp(value);
    int returned;
    temp >> returned;
    return returned; // Retourne la valeur si value était un nom "numérique", 0 dans le cas contraire.
}
int main(int argc, char* argv[]){

    std::ifstream subInput; // Ouverture des fichiers /proc/<PID>/status
    std::stringstream fileName; //  
    std::string buffer, name, value; // Name et Value sont des variables que l'on utilise pour parser le fcihier /proc/<PID>/status
    std::map<std::string,std::string> process; // "tableau associatif", on associe chaque propriété du processus à sa valeur
    DIR *dir; 
    struct dirent *subDir;
    dir = opendir("/proc/"); // On ouvre le /proc pour lister les processus
    while(subDir = readdir(dir)){ // tant qu'il y a des sous dossiers
        if(strToInt(subDir->d_name)){ // On vérifie si le nom du dossier est numérique
            fileName.str(""); // Vide le flux du nom du fichier
            fileName << "/proc/" << subDir->d_name << "/status"; // Récupération du status correspondant au processus qu'on lit
            subInput.open(fileName.str().c_str(), std::ios::in); 
            while(!subInput.eof()){ // Lecture de l'intégralité du fichier status du processus
                /* 
                   ce passage est le plus "complexe" du code, en effet, on récupère la ligne du fichier, et on la stocke dans un stringstream,
                   pourquoi ne pas directement lire les valeurs de la ligne dans les variables ?
                   Car comme vous avez pu le constater, une propriété à parfois plusieurs valeurs, il y aurait donc un problème lors de la lecture du fichier.
                   Si quelqu'un à une autre méthode (sans utiliser sscanf ou fscanf) cela m'interesse.
                */
                std::stringstream bufferString; 
                getline(subInput,buffer); 
                bufferString << buffer; 
                bufferString >> name >> value;
                process.insert(std::pair<std::string,std::string>(name,value)); // On associe la propriété à sa valeur
            }
            std::cout << process["Name:"] << " " << process["Pid:"] << " " << getpwuid(strToInt(process["Uid:"]))->pw_name << std::endl;
            /* 
               Affichage du résultat, tout simplement, petite subtilité pour récupérer le nom d'utilisateur à partir de l'UID, je vous revois à la fonction
               http://www.linux-kheops.com/doc/man/manfr/man-html-0.9/man3/getpwuid.3.html
            */
            subInput.close();
            process.clear();
        }
    }
}

Créeons notre fenêtre

Pour commencer, il nous faut la structure de notre fenêtre (image plus haut dans l'article), afin de créer un header qui contiendra chaque élément de la GUI, on créer donc un nouveau fichier : procWindow.hpp Voici sa structure de base :

#ifndef PROCESSWIN
#define PROCESSWIN
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <map>
#include <sys/types.h>
#include <signal.h> 
#include <dirent.h>
#include <pwd.h>
#include <gtkmm.h> 

class ProcessWindow : public Gtk::Window {
    public:


    protected:

};
#endif

On retrouve dans les inclusions les mêmes que tout à l'heure, qui servent à lister les processus, ainsi que le <gtkmm.h> qui est nécessaire à la compilation du projet ! On créer également une classe, nommée ProcessWindow héritant de Gek::Window, vous l'aurez compris, cette classe sera notre fenêtre qui contiendra toutes les fonctions dont nous avons besoin !

Attention : A partir de maintenant, dès lors que je ferai référence au header c'est de ce fichier .hpp dont je parlerai !

Il nous faut maintenant remplir cette classe, histoire de lui faire faire des trucs ! En "public", on a:

public:
        ProcessWindow();
        virtual ~ProcessWindow();
        void refreshProcessList();

Un constructeur par défaut, un déstructeur, et une methode, qui comme son nom l'indique, sert à réactualiser la liste de processus. Maintenant, nous devons mettre en "protected", tous ce qui nous servira dans l'interface, buttons, appel, fonction, sous-classes, en gros, pas mal de chose, et on se retrouve donc avec quelque chose comme:

protected:
        void processKill();
        void processReload();
        class ModelColumns : public Gtk::TreeModel::ColumnRecord{
            public:
                ModelColumns(){
                    add(m_col_pid);
                    add(m_col_name);
                    add(m_col_owner);
                }
                Gtk::TreeModelColumn<unsigned int> m_col_pid;
                Gtk::TreeModelColumn<Glib::ustring> m_col_name;
                Gtk::TreeModelColumn<Glib::ustring> m_col_owner;
        };
        ModelColumns m_Columns;
        Gtk::VBox m_VBox;
        Gtk::HBox m_HBox;
        Gtk::ScrolledWindow m_ScrolledWindow;
        Gtk::TreeView m_TreeView;
        Glib::RefPtr<Gtk::ListStore> m_refTreeModel;
        Gtk::HButtonBox m_ButtonBox;
        Gtk::Button m_Process_Kill;
        Gtk::Button m_Process_Reload;
        Gtk::Label killValue;
        int lastKill;

Je trouve les fonctions assez explicites, une pour tuer un processus, l'autre pour recharger la liste. La classe ModelColumns qui hérite de Gtk::TreeModel nous permet de personnaliser l'affichage de la liste, en y spécifiant le nombre de colonnes, le type de donnée ainsi que leur nom ou valeur. Dans notre constructeur ModelColumns() on ajoute donc trois colonnes, une de type integer qui affichera le PID du processus, et deux de type Glib:ustring (qui sont l'équivalent de std::string utilisées par GTKmm) pour le nom du processus et de son propriétaire.

Pour ce qui est des autres déclarations, je vous laisse vous referez à la documentation, mais on crée un conteneur vertical, un horizontal, ainsi qu'un permettant de scroller, deux boutons, un label pour afficher du texte, ne vous inquietez pas, je reviendrai sur chaque conteneur lorsque que nous allons écrire le fichier de notre classe, ce que nous allons faire de suite.

Remplir notre classe

Maitenant que nous avons notre header, il nous faut implementer toutes ces (nombreuses...) fonctionnalitées, pour cela, on crée un fichier "procWindow.cpp" que nous allons nous empresser de remplir, en commencant par le constructeur et le destructeur de notre classe, nous n'avons besoin que d'inclure un fichier, le header crée auparavant.

#include "procWindow.hpp" 

/* Constructeur de la classe */
ProcessWindow::ProcessWindow(): m_Process_Kill("Kill"), m_Process_Reload("Reload") {
    lastKill = -1;
    set_title("Process Viewer");
    set_border_width(5);
    set_default_size(400,200);

    add(m_VBox);
    m_ScrolledWindow.add(m_TreeView);
    m_ScrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
    m_VBox.pack_start(m_ScrolledWindow);
    m_VBox.pack_start(m_HBox,  false, false);
    m_HBox.pack_start(m_ButtonBox, Gtk::PACK_SHRINK);

    m_ButtonBox.pack_start(m_Process_Kill, Gtk::PACK_SHRINK);
    m_ButtonBox.set_border_width(5);
    m_ButtonBox.set_layout(Gtk::BUTTONBOX_END);

    m_ButtonBox.pack_start(m_Process_Reload, Gtk::PACK_SHRINK);
    m_ButtonBox.set_border_width(5);
    m_ButtonBox.set_layout(Gtk::BUTTONBOX_END);

    m_HBox.pack_start(killValue);

    m_Process_Kill.signal_clicked().connect(sigc::mem_fun(*this,&ProcessWindow::processKill));
    m_Process_Reload.signal_clicked().connect(sigc::mem_fun(*this, &ProcessWindow::processReload));
    m_refTreeModel = Gtk::ListStore::create(m_Columns);
    m_TreeView.set_model(m_refTreeModel);


    m_TreeView.append_column("Pid", m_Columns.m_col_pid);
    m_TreeView.append_column("Name", m_Columns.m_col_name);
    m_TreeView.append_column("Owner", m_Columns.m_col_owner);

    for(guint i = 0; i< 2;i++){
        Gtk::TreeView::Column *pColumn = m_TreeView.get_column(i);
        pColumn->set_reorderable();
    }

    show_all_children();
}

/* Destructeur */
ProcessWindow::~ProcessWindow(){

}

Voyons en détail ce que fait notre constructeur, et pourquoi il le fait :

  • mProcessKill("Kill"), mProcessReload("Reload") : On assigne juste un texte à nos deux boutons
  • set_* : le nom des fonctions semble relativement évident, on assigne une taille, un titre, une bordure, pour plus de détails, et de paramètres, la doc est là pour ca !
  • add(mVBox)_ : notre conteneur principal à l'intérieur de cette fenêtre est une VBox, c'est a dire que l'organisation se fait sous forme de ligne
  • mScrolledWindow.add(mTreeView) : notre second conteneur est une fenêtre de scroll, qui nous permettra de nous deplacer dans la liste des processus, on ajoute donc le "m_TreeView" à ce conteneur.
  • mScrolledWindow.setpolicy(Gtk::POLICYAUTOMATIC, Gtk::POLICYAUTOMATIC) : bien qu'anodine, cette fonction a toute son importance pour l'ergonomie de l'interface, on lui précise d'adapter la taille du scrolling à la taille de la fenêtre, mais également au nombre d'éléments présents
  • mVBox.packstart : la première ligne de notre m_Vbox contient donc notre barre de scrolling (et tout ce qui va avec), la seconde contient un conteneur de type horizontale qui lui même contient les boutons dont nous aurons besoin (je sais ce n'est pas facile à vous imaginez, mais regardez le premier schema lorsque vous avez un doute !
  • mProcessKill.signalclicked().connect(sigc::memfun(*this,&ProcessWindow::processKill)) : voici la partie la plus puissante de toute interface graphique, les "callbacks", en effet, un bouton ne sert pas à grand chose s'il ne fait rien quand on clique dessus, dans notre cas, on connecte chaque bouton, sur l'action clique ("clicked()") à une méthode de notre classe (kill ou reload)
  • mrefTreeModel = Gtk::ListStore::create(mColumns): on crée notre liste d'élément, en prenant comme patron la "sous-classe" que nous avons défini dans le header, et on ajuste la GUI sur ca (append_column), puis dans une boucle, on autorise le redimensionnement de chaque colonne.
  • FInalement, showallchildren() permet d'affiche la fenêtre et ce qu'elle contient !

Ayant eu pitié de vous (surtout ceux qui ont réussi à me lire jusqu'ici), j'ai rapidement réalisé un petit schema très rapide du positionnement : layout

Légende:

  • En rouge : m_VBox, conteneur principal, vertical
  • En vert : m_ScrolledWindow, permet de gérer le défilement
  • En bleu : m_TreeView, notre conteneur, avec ses colones
  • En jaune : m_HBox, pour contenir nos boutons et un label de texte !
  • En rose : m_ButtonBox, pour nos boutons

Nous avons fait le plus dur, il ne nous reste plus qu'a re-implementer le code du premier article en l'intégrant avec les callbacks, ce qui nous donne pour la fonction "refreshProcessList":

void ProcessWindow::refreshProcessList(){
    m_refTreeModel->clear();
    std::ifstream input, subInput;
    std::stringstream fileName;
    std::string buffer, name, value;
    std::map<std::string,std::string> process;
    DIR *dir;
    struct dirent *subDir;
    Gtk::TreeModel::Row row;
    dir = opendir("/proc/");
    while(subDir = readdir(dir)){
        if(strToInt(subDir->d_name) && strToInt(subDir->d_name) != lastKill){
            row = * (m_refTreeModel->append());
            fileName.str("");
            fileName << "/proc/" << subDir->d_name << "/status";
            subInput.open(fileName.str().c_str(), std::ios::in);
            while(!subInput.eof()){
                std::stringstream bufferString;
                getline(subInput,buffer);
                bufferString << buffer;
                bufferString >> name >> value;
                process.insert(std::pair<std::string,std::string>(name,value));
            }
            row[m_Columns.m_col_pid] = strToInt(process["Pid:"]);
            row[m_Columns.m_col_name] = process["Name:"];
            row[m_Columns.m_col_owner] = getpwuid(strToInt(process["Uid:"]))->pw_name;

            subInput.close();
            process.clear();
        }
    }
}

Le code principal ne change pas, on y ajoute juste des appels aux fonctions GTKmm afin d'afficher les résultats :

  • refTreeModel->clear() : vide la liste, afin de pouvoir le re-créer
  • row = * (m_refTreeModel->append()) : ajout d'une nouvelle ligne, pour chaque processus, que nous devons remplir
  • row[m_Columns.m_col_*] = *: une rempli la structure de la ligne avec les données récupérées dans le fichier "status"

De même avec la fonction pour tuer un processus :

void ProcessWindow::processKill(){
    Gtk::TreeModel::iterator iter = m_TreeView.get_selection()->get_selected();

    if(iter){
        Gtk::TreeModel::Row item = *iter;
        if(kill((pid_t)item[m_Columns.m_col_pid],1)== 0)
            killValue.set_text("Process Succesfully killed");
            lastKill = item[m_Columns.m_col_pid]; // Contient le dernier PID tué 
        refreshProcessList();
    }
}

Pour le coup, cette fonction est entièrement nouvelle par rapport à l'ancien code, étant donnée qu'elle se base sur des interactions avec GTKmm. On récupère la ligne séléctionnée dans la liste, et on fait appel à la fonction kill, qui prend en paramètre le PID du processus.

Vous avez peut-être remarqué que depuis le début, je trimballe une variable lastKill, dont je n'ai jamais parlé, et bien voici l'explication: Je me suis rendu compte, après avoir tué un processus, que ce-dernier ne disparaissait pas de la liste, même après avoir rafraichie cette dernière, le fait est qu'entre l'appel au kill() et la suppression du PID dans le /proc, il y a un delai, qui est trop grand et donc la fonction de rechargement ne prend pas en compte cette modification, j'ai donc triché, en stockant le dernier PID tué, afin de ne pas l'afficher dans la liste lors du rechargement.

Il ne nous manque plus que cette fameuse fonction qui appelle le rechargement, ainsi nous pouvons recharger très efficacement notre liste \o/ :

    void ProcessWindow::processReload(){
        refreshProcessList();
    }

Ah oui ! Dans le header, après la déclaration de la classe (donc après la "}" de la classe), il faut rajouter une petite méthode :

int strToInt(std::string value);

Cette fonction est utilisée lors de la récupération du PID depuis le fichier "/status" afin de le convertir en integer et de le stocker dans une ligne !

int strToInt(std::string value){
    std::istringstream temp(value);
    int returned;

    temp >> returned;
    return returned;
}

Notre classe est maintenant finie, mais il manque tout de même quelque chose, un main:

#include <gtkmm/main.h>
#include "procWindow.hpp"

using namespace std;

 // g++ main.cpp procWindow.cpp `pkg-config gtkmm-3.0 --cflags --libs`  

int main(int argc, char* argv[]){
        Gtk::Main kit(argc, argv);
        ProcessWindow window;
        window.refreshProcessList();
        Gtk::Main::run(window);
        return 0;
}

Pour résumer, voici les étapes de la réalisation d'une interface graphique avec GTKmm :

  • Création d'une classe qui hérite d'une Gtk::Window
  • Positionnement des éléments à l'intérieur (dans le constructeur), grâce aux conteneurs de GTKmm (scroll, vertical, horizontal et de nombreux autres)
  • Création des fonctions de callback sur les boutons de notre interface (ou les évènements, il ne s'agit pas forcement d'un clique sur un bouton)
  • Création d'un main afin d'instancier notre classe et de l'afficher

Projet complet en téléchargement : ici !

En conclusion, même si je ne suis pas un adepte de GUI, je dois dire que j'ai trouvé GTKmm très simple d'utilisation et très naturel à utiliser, on s'y habitue rapidement et on arrive à faire des interfaces simplistes sympathiques.

Analyse d'un protocole : BeeWi Helipad

16 Sep 2012

Notre cible est l'application “Beewi Heli” qui permet de controler un hélicopètre par bluetooth depuis son téléphone. Nous allons adapter l'application afin de pouvoir controler ce petit hélicoptère, en C++ (ce sera l'occasion d'étudier la gestion du bluetooth en C++).

==Nous aborderons les thèmes suivants : == * Décompilation et analyse d'une application Android * Gestion du bluetooth sur linux * Progammation et module bluetooth en C++ et Python

Analyse de l'application

Pour commencer, on se rend compte que si on lance l'application, notre ordinateur (avec le bluetooth allumé et détéctable) n'apparait pas dans la liste des periphériques, il saute au yeux que l'application effectue un tri sur les périphériques bluetooth avant de les afficher, afin vérifier cela, il va nous falloir : - jd_gui, un décompilateur Java - dex2jar, pour transformer le .apk en .java (code source java plus facilement lisible)

Le but de cet article n'est pas de vous montrer comment récupérer l'apk, il faut le récupérer depuis votre téléphone dans le dossier /data/app/ (explications SO) Il faut maintenant transformer ce .apk en .java pour pouvoir voir ce qu'il contient, pour cela, rien de plus simple:

sh d2j-dex2jar.sh beewi.apk 

vous sortira un magnifique fichier java que nous allons pouvoir analyser avec jd_gui :

./jd_gui beewi-dex2jar.java

Sur la gauche, s'affiche la liste des fichiers présents dans l'application nous allons nous interesser au fichier “SelectDeviceActivity” (n'oubliez pas que, pour le moment, la seule chose qui nous interresse est de comprendre le filtrage qu'effectue l'application).

Attention, ceci est un code décompilé et il peut y avoir des parties complétements étranges dans le code, le but n'étant pas de le modifier, mais de comprendre son fonctionnement, nous pouvons quand même tirer les grandes lignes de chaque fonction et en déceler le fonctionnement.

La fonction qui attire mon attention (et sûrement la votre) est

private static boolean isBluetoothCar(BluetoothDevice paramBluetoothDevice)

Une voiture ? Je pensais que l'on cherchait un hélicopètre ? Oui, mais une recherche sur le site de Beewi et on se rend compte qu'avant de developper un hélicoptère, la société vendait des voitures, en utilisant probablement un code similaire.

Cette fonction, pas très complexe à comprendre, ne se résume en fait qu'a une seul ligne, la dernière :

if (localBluetoothClass.getDeviceClass() == 1028);

La fonction vérifie que la classe du périphérique est bien 1028.

Un petit tour sur la doc android : http://developer.android.com/reference/android/bluetooth/BluetoothClass.Device.html nous apprend que 1028 (0x00000404) correspond à un appareil du type AUDIOVIDEOWEARABLEHEADSET_, ce n'est qu'un détail, mais la valeur héxadécimale est, elle très importante. Afin de vérifier notre hypothèse, modifions la classe du bluetooth de notre PC afin de voir s'il est découvert par l'application.

sudo hciconfig hci0 class 0x00000404 # hci0 est notre adaptateur bluetooth

Relancez l'application... Surprise ! Notre PC apparait bien dans la liste des appareils avec lequel on peut effectuer une connexion.

Petite astuce: si vous souhaitez modifier la classe de votre ordinateur, même après un redémarrage, il vous faudra éditer le fichier /var/lib/bluetooth/XX:XX:XX:XX:XX/config.

Nous aurions pu faire autrement (pour éviter d'avoir à décompiler l'application), nous aurions pu scanner l'hélicoptère afin de connaitre sa classe et de simplement la copier, cependant, cette technique ne nous donne pas la même classe, en effet: Pour scanner l'hélicoptère :

hcitool inq

Qui retourne une sortie plus ou moins similaire à

quiring ...
XX:XX:XX:XX:XX:XX   clock offset: 0x5cee    class: 0x240404

Ainsi, la classe de notre hélicoptère est égale à 0x240404.

Serveur bluetooth

Il faut maintenant que l'on code un serveur dont le rôle sera d'écouter les requêtes envoyées par l'application afin de comprendre le protocol et pouvoir forger nos propres requêtes. Le developpement d'une application bluetooth en C ou C++ étant lourd, et relativement complexe à faire, nous utiliserons donc python, avec la bibliothèque bluez (pour python...), pour effectuer nos tests, l'application finale, elle, sera codée en C++.

Vous l'aurez donc compris, l'hélicoptère fait office de serveur, et l'application, client se connecte à ce serveur, mais comment le client reconnait-t'il le serveur ? Sur quel port doit-il se connecter ? Afin de pouvoir répondre à ces questions, il va falloir se plonger un peu dans la documentation de PyBluez, la librairie bluetooth en Python Premièrement, il va falloir faire un scan pour detecter les appareils alentours (nous pourrions faire un scan avec hcitool, mais je souhaite développer un peu la programmation en python). Pour lister les appareils aux alentours, il faut utiliser la fonction discover_devices, sur la documentation, on apprend que le type de variable retourné par la fonction est une liste de tuple sous la forme (address, name).

La seconde fonction qui va nous être utile ici, est find_service, qui prend en paramètre une adresse, et/ou un uuid et/ou un nom, et qui retourne une liste de dictionnaires, avec différentes informations (énumérées dans la documentation), ainsi, la première fonction retourne l'adresse et le nom, nous sera utile pour appeller la seconde fonction, qui se chargera de mettre en évidence les différents services disponibles en bluetooth sur l'appareil. Dans les dictionnaires, seules les informations name, protocol et port nous seront utiles. Ainsi, le scanneur ressemble à quelque chose tel que :

# -*- coding: utf8 -*-
import bluetooth

devices = bluetooth.discover_devices(flush_cache=True, lookup_names=True);
for addr, name in devices:
    for service in bluetooth.find_service(address=addr):
        print 'Nom : %s '  % (service['name'])
        print ' Protocol : %s ' % (service['protocol'])
        print ' Port : %s ' % service['port']
    print ''

Ce petit bout de code, devrait sortir en retour quelque chose tel que :

XX:XX:XX:XX:XX:XX -> BeeWi BBZ301
 Nom : None
 Protocol : L2CAP
 Port : 1
 Nom : SerialPort
 Protocol : RFCOMM
 Port : 6

On voit qu'il y a deux services sur l'hélicoptère, un sur le port 1 et l'autre sur le port 6, c'est ce dernier qui va nous interesser (protocol RFCOMM). N'oubliez pas que notre but pour le moment est d'émuler un serveur afin de récupérer les commandes envoyés par le client, il faut donc créer un serveur qui écoutera les connexions entrantes sur le port 6, et fera une sauvegarde des données recues.

Encore une fois, un petit tour sur la documentation pour se renseigner sur la classe BluetoothSocket ainsi que sur les fonctions bind, listen, advertiseservice_ et le couple accept/recv (si vous avez déjà manipulé des sockets, vous devriez connaitre ces fonctions). Nous je m'attarderai pas sur toutes les fonctions volontairement, mais si vous avez des questions, n'hésitez pas à m'en faire part dans les commentaires.

La première fonction relativement complexe à utiliser est advertise_service, qui prend en paramètre une socket écoutant sur un port (bind et listen), le nom du service (qui devra être celui de l'hélicoptère), service_classes, qui nous initialiserons comme indiqué dans la documentation à SERIALPORTCLASS et le profile, que nous laisserons également comme indiqué dans la documentation, c'est à dire à SERIALPORTPROFILE.

Le reste est relativement simple, on créer une socket bluetooth du type RFCOMM (comme celui de l'hélicoptère), il faut ensuite binder cette socket sur un port (le 6 dans notre cas) grâce à la fonction bind() et puis, il faut mettre la socket en écoute, à l'aide de listen(), ensuite, on créer un service comme expliqué précédement, et on accepte la première connexion entrante (en créant une nouvelle socket). Pour finir, on fait une boucle infinie, dans laquelle on recoit des données du clients grâce à la fonction recv().

Le code final ressemblerait donc à quelque chose comme ca :

# -*- coding: utf8 -*-
import bluetooth

server=bluetooth.BluetoothSocket( bluetooth.RFCOMM )
 # Car le serveur de l'hélicoptère utilise ce protocol)
server.bind(("",6))
 # Bind sur le port 6
server.listen(1)
  # Attente d'une connexion (et une seule)

bluetooth.advertise_service(server, "SerialPort",
                                service_classes=[bluetooth.SERIAL_PORT_CLASS],
                                profiles=[bluetooth.SERIAL_PORT_PROFILE])
 # Création d'un service similaire à celui de l'hélicoptère

client, address = server.accept()
 # On accepte la première connexion 
print "Connection depuis : ",address

while client:
        data = client.recv(1024)
        print "Données recues : [%s]" % data
        stop = raw_input()


client_sock.close()
server_sock.close()

En le lancant et en connectant l'application, on se rend compte que les requetes ont la forme: Données recues : [0x1500000000FF] Et que cette valeur change en fonction des différentes variables (vitesse, hauteur, inclinaison)

Je sais que certains d'entre vous sont déçus car je n'ai pas tenu ma promesse (bluetooth en C++), alors je me suis penché sur le problème qu'est la bibliothèque Bluez en C++, la documentation de celle-ci n'existant tout simplement pas, vous imaginez que ca ne facilite pas les choses... Enfin voilà, après quelques jours de recherches (principalement sur les sites : people.csail.mit.edu et btessentials.com. Après avoir bataillé des heures, voici une application fonctionelle permettant de faire exactement la même chose que le code python juste au dessus.

/*
 * Ecrit par : Ex0ns
 * Site : ex0ns.me
 * Sources : http://people.csail.mit.edu/albert/bluez-intro/x604.html et http://people.csail.mit.edu/rudolph/Teaching/Articles/BTBook.pdf
 * Bibliothèque utilisées : bluez
 * Compilation : g++ advertise.cpp -o advertise -lbluetooth
 */

#include <iostream> 
#include <unistd.h> // int close(int fd)
#include <sys/types.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/sdp.h>  // Fonction sdp_* (pour créer un service bluetooth)
#include <bluetooth/sdp_lib.h> // sdp_session, sdp_close
#include <bluetooth/rfcomm.h> // struct sockadd_rc

#define BDADDR_ANY_INITIALIZER   0 // Définition pour régler un prolème de temporary addresses lié à l'utilisation de BDADDR_ANY
#define BDADDR_LOCAL_INITIALIZER 0 // De même mais pour BDADDR_LOCAL

using namespace std;

/*
 * Cette fonction, plutot complexe, permet d'ajouter un service à la liste des services disponible,
 * C'est l'équivalent de la fonction bluetooth.advertise_service() en python
 * La documentation sur la bibliothèque Bluez étant inexistante, cette fonction peut contenir des erreurs, mais est fonctionnelle.
 */
sdp_session_t *advertise_service()
{
    uint8_t rfcomm_port = 6; // Numéro de Port
    const char *service_name = "SerialPort"; // Nom du service
    uuid_t root_uuid, l2cap_uuid, rfcomm_uuid,  svc_class_uuid;
    sdp_list_t *l2cap_list = 0,
               *rfcomm_list = 0,
               *root_list = 0,
               *proto_list = 0, // Contient tous les protocoles utilisés par le service (RFCOMM et L2CAP)
               *access_proto_list = 0,
               *svc_class_list = 0,
               *profile_list = 0;
    sdp_data_t *channel = 0;
    sdp_profile_desc_t profile;
    sdp_record_t record = { 0 };
    sdp_session_t *session = 0;

    // Informations sur la classe, équivalent au paramètre  service_classes=[bluetooth.SERIAL_PORT_CLASS] dans le code python
    sdp_uuid16_create(&svc_class_uuid, SERIAL_PORT_SVCLASS_ID);
    svc_class_list = sdp_list_append(0, &svc_class_uuid);
    sdp_set_service_classes(&record, svc_class_list);

    // Informations sur le profile, équivalent au paramètre profiles=[bluetooth.SERIAL_PORT_PROFILE]) dans le code python
    sdp_uuid16_create(&profile.uuid, SERIAL_PORT_PROFILE_ID);
    profile.version = 0x0100;
    profile_list = sdp_list_append(0, &profile);
    sdp_set_profile_descs(&record, profile_list);


    // Rendre le service publique et detectable
    sdp_uuid16_create(&root_uuid, PUBLIC_BROWSE_GROUP); 
    root_list = sdp_list_append(0, &root_uuid);
    sdp_set_browse_groups( &record, root_list );

    // Informations sur le protocol L2CAP (obligatoire pour la connexion, je n'ai toujours pas trouvé pourquoi)
    sdp_uuid16_create(&l2cap_uuid, L2CAP_UUID);
    l2cap_list = sdp_list_append( 0, &l2cap_uuid );
    proto_list = sdp_list_append( 0, l2cap_list ); // Ajout du L2CAP à la liste des protocoles du service

    // Création de la socket RFCOMM
    sdp_uuid16_create(&rfcomm_uuid, RFCOMM_UUID);
    channel = sdp_data_alloc(SDP_UINT8, &rfcomm_port);
    rfcomm_list = sdp_list_append( 0, &rfcomm_uuid );
    sdp_list_append( rfcomm_list, channel ); // Précise quel port (canal) utiliser

    sdp_list_append( proto_list, rfcomm_list ); // Ajout du protocol RFCOMM à la liste de ceux du service
    access_proto_list = sdp_list_append( 0, proto_list );
    sdp_set_access_protos( &record, access_proto_list );

    // Phase finale de création du service, on donne le nom, et accesoirement une description et un "fourniseur"
    sdp_set_info_attr(&record, service_name, NULL, NULL);


    // Il faut maintenant contacter le serveur SDP local pour créer et enregister un nouveau service
    bdaddr_t any  = BDADDR_ANY_INITIALIZER; // Voir explications (commentaires suivants)
    bdaddr_t local = BDADDR_LOCAL_INITIALIZER; // De même
    session = sdp_connect(&any, &local, 0 ); // On contact le serveur sdp
    /*
     * Alors là, petit particularité, en théorie, la fonction sdp_connect s'appelle de la façon suivante :
     * session = sdp_connect(BDADDR_ANY, BD_ADDR_LOCAL, 0 );, mais en faisant ca, on obtient une erreur à la compilation,
     * Pour corriger on déclare deux constantes au début, BDADDR_ANY_INITIALIZER et BDADDR_LOCAL_INITIALIZER, que nous utiliserons
     */
    sdp_record_register(session, &record, 0); // On ajoute notre service à la liste des services

    // Il faut maintenant faire un peu de nettoyage dans nos structures
    sdp_data_free( channel );
    sdp_list_free( l2cap_list, 0 );
    sdp_list_free( rfcomm_list, 0 );
    sdp_list_free( root_list, 0 );
    sdp_list_free( access_proto_list, 0 );

    return session;
}

int main(){
    /*
     * Création d'un service bluetooth et écoute des données envoyées par le client
     * Pour reverse un protol BeeWi Helipad
     * Voir : http://ex0ns.hostei.com/article-6-Analyse-d-un-protocole-BeeWi-Helipad pour plus d'informations
     */
    int sock, client; // Nos deux sockets (client et serveur)
    socklen_t addrlen; // Taille de la structure qui contiendra les informations clients
    unsigned char buff[14];
    struct sockaddr_rc addr; // Structure des informations clients

    sock = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); // Création d'une socket bluetooth

    addr.rc_family = AF_BLUETOOTH; // On précise la famille (tout comme pour INET ou INET6)
    bdaddr_t any = BDADDR_ANY_INITIALIZER; // Même problème, vois explications dans la fonction ou au niveau de #define
    bacpy(&addr.rc_bdaddr, &any);
    addr.rc_channel = 6; // Port utilisé

    bind(sock, (struct sockaddr *)&addr, sizeof(addr)); // Rien de très étonnant
    listen(sock, 1); // On écoute un client

    sdp_session_t *session = advertise_service(); // On crée notre service, sur le même port
    addrlen = sizeof(addr); 
    client = accept (sock, (struct sockaddr *)&addr, &addrlen); // Création de la socket client
    while(client){ // tant que le client est connecté
        recv(client, buff, 14, 0);
        cout << buff;
    }
    close (client); // Fermeture des sockets
    close (sock);
    sdp_close(session); // Fermeture du service 
    return 0;
}

Notez que vous pouvez également trouver ce code, mieux présenté, sur pastebin.

Analyse des données

Nous pouvons déjà remarquer les deux premiers bytes et les deux derniers semblent être fixes, nous les appellerons maintenant prefixe pour 0x15 et suffixe pour 0xFF. Lançons le serveur, et jouons un peu sur l'application pour voir quelles sont les informations modifiées lorsque l'on accelère. On se rend compte qu'en fonction de l'accélération, les données passent de 0x1500000000FF à 0x15FF000000FF donc, que la valeur varie entre 0x00000000FF et 0xFF000000FF (le but va être de créer un variable pour chaque donnée et de recomposer la valeur finale avant de l'envoyer). La vitesse du trimmer (stabilisation), elle, peut, d'après l'application, varier entre 0x14000000f8FF et 0x17000000f8FF, on peut également noter si la vitesse du trimmer est nulle, alors le préfixe vaut 0x15, quand la vitesse est positive (sens de rotation positif), le préfixe passe à 0x17, et si le sens de rotation est contraire, alors le préfixe devient 0x14. On remarque que les deux avant derniers chiffres varient en fonction de la vitesse, à une echelle de 0x08, ainsi, la gestion de ces deux paramètres est facile à implementer, mais il reste la plus complexe, celle de la direction, et c'est pas du gateau (il faut d'ailleurs que je finisse de comprendre le fonctionnement avant d'écrire la fin de cet article).

Edit du 11/10/12

Voici une petite interface de contrôle, alors j'entend par petit, basique, en effet, écran noir, 800x600, adresse mac dans le code source, pas très sympa, mais fonctionnelle. Elle permet de controler le rotor et le trimmer de l'hélicoptère assez facilement et donne un petit apercu de la SFML (malheureusement, la vitesse n'est toujours pas manipulable). Sans plus attendre, voici le code (disponible sur pastebin).

/*
    Ultimate BeeWi HeliPad controler
    Used Lib : sfml-1.6, bluez
    Author : ex0ns (ex0ns.hostei.com)
    Langage : C++
    Compilation : g++ controler.cpp -o controler -lbluetooth -lsfml-window -lsfml-system
*/
#include <iostream>
#include <sstream>

#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>

#include <SFML/System.hpp> // SFML 
#include <SFML/Window.hpp>
#include <SFML/Network.hpp>

typedef long long int lli;

const char *intToChar(lli number){
    /* This Function is use to create an hex string from a number), so it can be pass as parameter to the socket */
    std::stringstream ss(std::stringstream::in | std::stringstream::out);
    ss << "0x" << std::hex << number;
    return ss.str().c_str();
}

int main()
{
    /* Variables For the Bluetooth Connection */
    struct sockaddr_rc addr = {0};
    char server[18] = "00:11:67:E3:DE:63"; // MAC adress of our Helicoptere
    int s, status; // Two socket handler

    /* Variables For the Flying Monitor */
    lli prefixe = 0x150000000000; // See the article for more details
    lli suffixe = 0x0000000000FF;
    lli firstRotorSpeed = 0x0000000000;
    lli trimmerValue = 0x0000;

    lli firstRotorSpeedCoeff = 0x0500000000; // Rotor speed increase
    lli trimmerCoeff = 0x0800; // Trimmer Modification

    lli command; // Final Command sent to the Heli
    int trimmerPosition = 0; // 0 = Null, 1 = Positif (right), -1 (negative)

    /* Handle Bluetooth Connection */
    s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM); // See the article for more details
    addr.rc_family = AF_BLUETOOTH;
    addr.rc_channel = (uint8_t)6;
    str2ba(server, &addr.rc_bdaddr);

    status = connect(s, (struct sockaddr *)&addr, sizeof(addr));

    if(status == 0){
        sf::Window App(sf::VideoMode(800,600,30), "Helipad Controler"); // Just show a black screen I didn't take the time to create a beautiful nice GUI 
        sf::Clock clock;
        bool running = true;
        while(App.IsOpened()){

            sf::Event Event;
            while(App.GetEvent(Event)){
                if(Event.Type == sf::Event::Closed) // Hook the "cross" button to quit
                    App.Close();

                if(Event.Type == sf::Event::KeyPressed && (Event.Key.Code == sf::Key::PageUp)){ // Increase First Rotor Velocity
                    if(firstRotorSpeed <= 0xFF00000000-firstRotorSpeedCoeff)
                        firstRotorSpeed+=firstRotorSpeedCoeff;
                } 
                if(Event.Type == sf::Event::KeyPressed && (Event.Key.Code == sf::Key::PageDown)){ // Descrease First Rotor Velocity
                    if(firstRotorSpeed >= firstRotorSpeedCoeff)
                        firstRotorSpeed-=firstRotorSpeedCoeff;
                }

                if(Event.Type == sf::Event::KeyPressed && (Event.Key.Code == sf::Key::Subtract)){ // Reduce trimmer velocity 
                    /*
                     * The Subtract key is the '-' on the numPad
                     */
                    if(trimmerValue > trimmerCoeff && trimmerPosition == 1 ){ 
                        trimmerValue-=trimmerCoeff;
                        prefixe = 0x170000000000;                       
                    }
                    else if(trimmerValue == trimmerCoeff && trimmerPosition == 1){ 
                        trimmerValue = 0x0000;
                        prefixe = 0x150000000000;
                        trimmerPosition = 0;
                    }
                    else if(trimmerValue <= 0xF800-trimmerCoeff && trimmerPosition != 1){ 
                        trimmerValue+=trimmerCoeff;
                        prefixe = 0x140000000000;
                        trimmerPosition = -1;
                    }
                }

                if(Event.Type == sf::Event::KeyPressed && (Event.Key.Code == sf::Key::Add)){ // Increase trimmer velocity
                    /*
                     * The Add key is the '+' on numPad
                     */
                    if(trimmerValue > trimmerCoeff && trimmerPosition == -1 ){  
                        trimmerValue-=trimmerCoeff;
                        prefixe = 0x140000000000;                   
                    }
                    else if(trimmerValue == trimmerCoeff && trimmerPosition == -1){ 
                        trimmerValue = 0x0000;
                        prefixe = 0x150000000000;
                        trimmerPosition = 0;
                    }
                    else if(trimmerValue <= 0xF800-trimmerCoeff && trimmerPosition != -1){ 
                        trimmerValue+=trimmerCoeff;
                        prefixe = 0x170000000000;
                        trimmerPosition = 1;
                    }
                }
            }

            App.Display();
            if(clock.GetElapsedTime() > 0.5){ // Timer To Avoid Request Spamming
                command = prefixe + firstRotorSpeed + trimmerValue + suffixe;
                std::cout << intToChar(command) << std::endl;
                status = write(s, intToChar(command), 14); // We know that we have to send 14 bytes by analysing the requests
                clock.Reset(); // To reset timer so GetElapsedTime can work again
            }
        }
    }else{
        std::cout << "Connection Failed" << std::endl;
    }

    close(s);
    return 0;
}

Voilà, rien de bien sorcier, je projette, si je trouve le temps, de vous en proposer une plus sympa, avec deux ou trois boutons, une recherche des périphériques à porté, gestion des touches (préférences) mais le code sera tout de suite plus conséquent !

Gestion des processus sur GNU/Linux

01 Jul 2012

D'après Wikipédia (qui lui même cite la norme ISO 9000), un processus est:

L’ensemble d’activités corrélées ou interactives qui transforme des éléments d’entrée en éléments de sortie

Plus clairement, on retrouve quelques lignes plus bas, une autre définition du terme processus:

Un processus est une tâche en train de s’exécuter. On appelle processus l’image de l’état du processeur et de la mémoire au cours de l’exécution d’un programme.

Vous l’aurez compris, un processus est une tache en cours d’exécution sur votre système d’exploitation, on peut citer votre navigateur, serveur graphique (Xorg).

Le noyau Linux fournit des informations à l’utilisateur à travers un système de fichier nommé “proc”, que se situe à la racine : “/proc”, ce dossier contient de nombreux fichiers contenant des informations concernant le noyau (vous pouvez trouver dans ces fichiers des informations sur l’utilisation des différents processeur, la température de l’ordinateur, la vitesse de ventilation).

Mais comme vous l’avez remarqué de nombreux dossiers sont également présents, la plupart d’entre eux ont un nom numérique (entre 1 et 6 chiffres), chacun de ces dossiers correspond à un processus, il porte le PID (Process Identifier) du processus correspondant, pour connaitre le PID d’un processus, il vous suffit d’entrer la commande : “pidof ” .

Ainsi chaque sous dossier du “/proc” est un dossier contenant les informations sur un processus (correspondant au nom du dossier), à partir de là, il est possible de rassembler énormément d’informations sur ce dernier, c’est ce que nous allons faire, à travers un “gestionnaire de processus” codé en C++. Pour la première partie, les connaissances requises sont faibles, nous n’allons que manipuler des flux (fstream, sstream) et dans une seconde partie, nous élaborerons une petite interface graphique à l’aide de Gtkmm, dont le code sera explicité un minimum, mais étant donné qu’il ne s’agit pas du but de cet article, il pourra subsister des zones d’ombre (un article sur gtkmm est prévu, car cette bibliothèque graphique est très sympathique).

Chaque “dossier processus” du “/proc” contient de nombreuses informations relatives au fonctionnement de ce dernier, vous pouvez fouiller un peu dans ces dossiers/fichiers, ils ne vous mordront pas.

Le fichier auquel nous allons nous intéresser est le fichier “/proc/<PID>/status” , il contient le nom, l’id, l’UID (user identifier) et beaucoup d’autres informations qui sont moins intéressantes pour nous, mais utiles à certaines occasions.

Le fichier “status” se compose de la façon suivante :

Nom: valeur

Parfois il y a plusieurs valeurs, mais nous nous intéresserons qu’a la première valeur du nom (pour faciliter la récupération d’informations lors du parsage du fichier). Votre mission, si vous l’acceptez, est de lister tous les processus qui tournent sur le système : nom, PID, et le nom de leur propriétaire (UID).

Voici le code :

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <map>
#include <sys/types.h>
#include <dirent.h>
#include <pwd.h>

int strToInt(std::string value){
    std::istringstream temp(value);
    int returned;
    temp >> returned;
    return returned; // Retourne la valeur si value était un nom "numérique", 0 dans le cas contraire.
}
int main(int argc, char* argv[]){

    std::ifstream subInput; // Ouverture des fichiers /proc/<PID>/status
    std::stringstream fileName; //  
    std::string buffer, name, value; // Name et Value sont des variables que l'on utilise pour parser le fcihier /proc/<PID>/status
    std::map<std::string,std::string> process; // "tableau associatif", on associe chaque propriété du processus à sa valeur
    DIR *dir; 
    struct dirent *subDir;
    dir = opendir("/proc/"); // On ouvre le /proc pour lister les processus
    while(subDir = readdir(dir)){ // tant qu'il y a des sous dossiers
        if(strToInt(subDir->d_name)){ // On vérifie si le nom du dossier est numérique
            fileName.str(""); // Vide le flux du nom du fichier
            fileName << "/proc/" << subDir->d_name << "/status"; // Récupération du status correspondant au processus qu'on lit
            subInput.open(fileName.str().c_str(), std::ios::in); 
            while(!subInput.eof()){ // Lecture de l'intégralité du fichier status du processus
                /* 
                   ce passage est le plus "complexe" du code, en effet, on récupère la ligne du fichier, et on la stocke dans un stringstream,
                   pourquoi ne pas directement lire les valeurs de la ligne dans les variables ?
                   Car comme vous avez pu le constater, une propriété à parfois plusieurs valeurs, il y aurait donc un problème lors de la lecture du fichier.
                   Si quelqu'un à une autre méthode (sans utiliser sscanf ou fscanf) cela m'interesse.
                */
                std::stringstream bufferString; 
                getline(subInput,buffer); 
                bufferString << buffer; 
                bufferString >> name >> value;
                process.insert(std::pair<std::string,std::string>(name,value)); // On associe la propriété à sa valeur
            }
            std::cout << process["Name:"] << " " << process["Pid:"] << " " << getpwuid(strToInt(process["Uid:"]))->pw_name << std::endl;
            /* 
               Affichage du résultat, tout simplement, petite subtilité pour récupérer le nom d'utilisateur à partir de l'UID, je vous revois à la fonction
               http://www.linux-kheops.com/doc/man/manfr/man-html-0.9/man3/getpwuid.3.html
            */
            subInput.close();
            process.clear();
        }
    }
}

Sur pastebin : http://pastebin.com/svk6tY9m

Nous verrons dans la suite, comment faire une interface graphique simple à l'aide de GTKmm en C++.

En espérant avoir été clair !