Tutorat système & Jeu des chaises musicales

Thomas Vantroys & Xavier Redon

1   Description générale du projet

1.1   Objectif

L'objectif est de concevoir des accessoires vestimentaires, une ceinture et un bracelet en l'occurrence, et d'écrire un logiciel Unix permettant de jouer à une variante d'un jeu collectif bien connu.

1.2   Règle du jeu

Avant le lancement d'une partie, un joueur passe en mode animateur. En début de chaque manche, l'animateur prévient les autres joueurs de se tenir prêt puis effectue un geste du bras pour appliquer une accélération au bracelet dont il est équipé. On distingue 6 accélérations différentes, deux suivant chaque axe. Les autres joueurs doivent mimer le geste le plus rapidement possible. Les joueurs n'effectuant pas le bon geste sont éliminés et le joueur le plus lent sont éliminés. La partie continue jusqu'à qu'il reste au plus un joueur.

2   Description des composants du projet

2.1   Architecture générale

Les accessoires permettent d'informer les joueurs des différentes phases de jeu et de remonter l'accéleration des bracelets vers un système distribué de processus Unix permettant de gérer le jeu.

La transmission des données se fait par communication sans fil XBee entre les accessoires et un processus Unix tournant sur le PC associé aux accessoires puis par TCP/IP entre les différents processus Unix.

Chaque processus Unix gère le score indépendamment des autres processus à l'aide des informations remontés des accessoires et diffusés en UDP par les autres processus. Un processus doit aussi gérer des connexions TCP pour permettre aux utilisateurs d'avoir des informations sur le déroulement du jeu (comme l'état des joueurs par exemple).

2.2   Conception de l'application UDP distribuée

Le principe est d'avoir une architecture distribuée pour implanter la gestion du jeu. Aucun processus Unix n'aura de rôle prédominant et chacun doit pouvoir tenir à jour l'état de son joueur en fonction des messages échangés par diffusion UDP.

Vous commencerez par attribuer un numéro unique à chaque processus par échange de messages. Le processus tire un numéro au hasard, envoi un message pour vérifier qu'il est libre et recommence s'il ne l'est pas.

Chaque processus s'annonce réguliérement à l'aide de son numéro unique et signale ainsi sa présence aux autres processus qui peuvent établir un tableau des joueurs.

Une partie débute lorsqu'un et un seul animateur est présent et qu'il indique son intention de lancer le jeu. A partir de ce moment les processus qui se sont déjà signalés savent qu'ils participent au jeu et arrêtent de se signaler. Les processus se signalant après ne peuvent plus participer à ce jeu. Pour gérer le cas des participants tardifs, c'est à dire se signalant en même temps que le lancement du jeu, les joueurs se signalant en cours de première manche sont pris en compte.

Durant un jeu, l'animateur prévient qu'il va effectuer un geste et effectue ce geste. Les deux événements sont diffusés par UDP. Lors de la réception de l'avertissement les autres processus se mettent à leur tour en attente du geste de leur joueur. Le geste est diffusé dès sa réception. Si un processus s'aperçoit qu'il est le dernier, il peut annuler son attente et diffuse un message de geste trop tardif. Cette procédure permet à chaque joueur de savoir s'il est éliminé ou non.

Le processus de l'animateur laisse son joueur relancer une manche après un temps court faisant suite à la réception du premier geste correct (ou du dernier geste si aucun n'est correct). Les joueurs éliminés restent inactifs jusqu'à la fin de la partie. Une partie se termine quand au plus un joueur reste en lice.

Le format des messages UDP est laissé à votre bon soin. Il est évident qu'un format unique doit être adopté par la promotion.

2.3   Les requêtes des clients TCP

Un utilisateur peut se connecter sur le port TCP d'écoute d'un quelconque processus Unix. Un processus Unix doit pouvoir gérer plusieurs processus Unix simultanément. La connexion TCP se fait avec un utilitaire comme nc. Les ordres que l'utilisateur a à sa disposition sont :
aide :
obtenir la liste des ordres possibles ;
joueurs :
donner la liste des joueurs (identifiant et nom de machine hôte) s'étant signalés ou en compétition, le type de chaque joueur (animateur ou compétiteur) doit être précisé ;
score :
si un jeu est en cours, donner l'état des joueurs, c'est à dire s'ils sont encore en lice ou à quelle manche ils ont été éliminés ;
suivre :
suivre les événements du jeu, ces événements sont affichés, au fur et à mesure, sur le terminal de l'utilisateur, parmis les événements on trouve le début d'une partie (comprenant l'identifiant de l'animateur), la fin d'une partie (comprenant l'identifiant de l'éventuel gagnant), le lancement d'une manche (comprenant son numéro), le geste effectué par l'animateur et enfin les gestes effectués par les joueurs ;
stopper :
arrêter le processus Unix.
Pour implanter la commande suivre, il est recommandé de définir, au niveau du processus, un tableau des clients TCP en attente d'événements. Ainsi à chaque nouveau événement à leur transmettre, il suffit de parcourir ce tableau et de les contacter sur leur connexion TCP.

2.4   Les accessoires

Chaque binôme reçoit deux accessoires vestimentaires, une ceinture et un bracelet en tissu, qui serviront de support aux composants. Ces composants sont un module Lilypad intégrant un micro-contrôleur atmega328p, un module de communication série sans fil XBee et divers capteurs et actionneurs ; un interrupteur, un bouton, un accéléromètre, une led RVB, un vibreur et un buzzer. L'ensemble des composants sont fixés sur la ceinture mis à part l'accéléromètre qui se trouve sur le bracelet. La fixation des composants sur le tissu se fait à l'aide de fermoirs à pin's. Les connexions entre les composants se font par des pinces crocodiles ou des fils enroulés.

La programmation du micro-contrôleur se fait en utilisant le compilateur avr-gcc. L'algorithme à implanter sur le micro-contrôleur consiste en la lecture des ordres arrivant sur le port série et en la réalisation des actions ou mesures demandées. Les valeurs mesurées sont renvoyées via le même port série.

Prenez soin de configurer vos deux modules XBee pour qu'ils puissent communiquer entre eux sur une fréquence leur étant propre.

Les actions devant être réalisées par les accessoires sont décrit dans le tableau ci-dessous.

Evénement Actions
Test de présence du contrôleur retour de l'état de l'interrupteur, LED bleue
Indication de présence de plusieurs animateurs LED clignote rouge et mélodie à deux tons
Indication d'unicité de l'animateur LED clignote vert
Indication de signalisation de présence Bip sonore
Attente de lancement de partie Longue vibration répétée jusqu'à pression sur le bouton
Attente de lancement de manche Attente de 5 secondes, courte vibration répétée jusqu'à pression sur le bouton
Partie demarrée LED verte
Attente de geste LED verte à intensité variable, scrutation de l'accéléromètre limité à 15 secondes, retour du type de geste
Joueur éliminé LED rouge et mélodie "game over" de mario
Joueur gagnant Mélodie joyeuse, LED clignotante alternativement verte et bleu

3   Organisation du travail

3.1   Généralités

Il est fortement conseillé de suivre les étapes proposées ci-après pour réaliser le travail.

3.2   Organisation modulaire

Le projet est constitué du programme pour le micro-contrôlleur, de 2 bibliothèques et du programme tournant sous Unix. Chacune de ces entités est développée dans un répertoire propre. On gère donc les quatre répertoires suivants :
Costume -
pour les sources du programme chargé sur la plate-forme Arduino Lilypad ;
Communication -
pour les sources de la bibliothèque contenant les fonctions de gestion réseau et série libcom ;
Threads -
pour les sources de la bibliothèque contenant les fonctions de gestion des threads libthrd ;
Jeu -
pour l'application de gestion du jeu et de communication avec les accessoires.
Cette arborescence et quelques squelettes de fichiers sont disponibles sous forme d'un fichier au format tar et compressé à l'URL http://www.plil.net/~rex/Enseignement/Systeme/Tutorat.IMA4sc.Chaises/chaises.tgz. Transférez ce fichier dans votre compte Polytech'Lille et décompressez-le avec la commande tar xvzf chaises.tgz. Vous pouvez constater que très peu de code est fourni concernant l'application de gestion des accessoires et du jeu : Cependant, le répertoire créé contient déjà un fichier Makefile global et des Makefile annexes dans chaque sous-répertoire. Vous avez un exemple de Makefile permettant de générer un exécutable et un exemple de Makefile permettant de construire une bibliothèque. Appuyez vous sur ces deux exemples pour compléter les autres Makefile. Le projet doit pouvoir être généré par la simple commande make exécutée dans le répertoire principal du projet. Il doit être aussi possible de regénérer complètement le projet par la commande make clean all lancée du même répertoire.

Vous constaterez aussi la présence d'un répertoire XBee contenant un utilitaire qui peut vous aider pour configurer vos modules XBee. Le code de cet utilitaire est suffisament clair pour que vous puissez en comprendre le fonctionnement en le lisant.

On prévoira de pouvoir compiler les différents sources avec un drapeau DEBUG (option -DDEBUG de gcc), permettant un affichage conditionnel d'informations de déverminage des programmes.

3.3   Sockets et serveur TCP

Il s'agit dans cette étape de réaliser un serveur TCP basique à l'aide de l'interface de programmation des sockets.

Ecrivez dans le module libcom.c (répertoire Communication), les deux fonctions suivantes : Testez cette bibliothèque en écrivant un programme jeu.c (répertoire Jeu) qui l'utilise et dont la fonction de traitement des connexions (celle appelée par boucleServeur) effectue juste une écriture de message dans la socket et clôt la connexion. Pour écrire, et plus tard lire, sur la connexion utilisez les fonctions classiques comme fgets ou fprintf. Pour y arriver vous devez transformer le descripteur de socket en une structure de fichier par la primitive fdopen.

Ce programme peut prendre des arguments : -p <port> ou --port <port> pour spécifier un numéro de port différent de celui par défaut (port 4000). Pour traiter les arguments, utilisez la fonction getopt_long (voir la page de manuel correspondante). Si les arguments sont incorrects, on doit afficher un message qui précise la syntaxe. Pour plus de clarté, l'analyse des arguments et l'affichage de la syntaxe seront écrits dans des fonctions séparées.

Modifiez la fonction de traitement des connexions afin qu'elle ne ferme la connexion qu'après avoir lu une commande de l'utilisateur. Testez votre serveur avec plusieurs commandes nc simultanées. Conclusion ?

3.4   Un serveur de jeu à base de processus légers

Pour que votre serveur de jeu puisse traiter plusieurs connexions simultanément, vous allez lancer un processus léger (thread) par connexion. Pour cela, implémentez la fonction publique de libthrd (répertoire Threads) :

int lanceThread(void (*)(int),int);
Cette fonction doit avoir comme action de lancer un thread dans le mode détaché. Ce thread doit exécuter la fonction passée en paramètre. La dite fonction prenant elle même comme paramètre le second paramètre de lanceThread. Il vous est conseillé d'utiliser une fonction intermédiaire récupérant un pointeur vers une structure comprenant les deux paramètres de lanceThread.

Testez votre fonction lanceThread en créant une nouvelle fonction dans jeu.c qui appelle votre fonction de traitement des connexions via lanceThread. Utilisez la nouvelle fonction comme paramètre de boucleServeur. Pour plus de clarté, ce serait une bonne idée de déplacer les fonctions de traitement des ordres des utilisateurs du fichier jeu.c vers le fichier gestionOrdres.c.

Vérifiez que votre serveur est maintenant capable de gérer plusieurs connexions simultanément (toujours avec l'utilitaire nc).

La fonction lanceThread est utilisée dans plusieurs situations différentes. Tout d'abord comme expliqué plus haut pour gérer les ordres des utilisateurs ; dans ce cas l'entier passé à la fonction paramètre est un descripteur de socket de dialogue. La fonction lanceThread est aussi utilisée pour démarrer le processus léger de gestion des messages UDP et le processus léger de gestion du jeu.

3.5   Diffusion de messages par UDP

Dans la description de l'architecture générale il est indiqué que les processus Unix doivent s'échanger des messages par UDP. Vous allez donc ajouter dans la bibliothèque Communication les fonctions suivantes : Testez vos fonctions en créant un fichier gestionMessages.c. Vous y écrirez une fonction messages permettant de lancer un processus léger réalisant principalement un appel à la fonction serveurMessages. La fonction de traitement associée se bornera à afficher le contenu des messages reçus en considérant qu'il s'agit d'une chaîne de caractères. Dans le même fichier écrivez une fonction qui va créer un message avec une chaîne quelconque et l'envoyer.

Dans l'embryon du fichier de gestion du jeu, que l'on va nommer algorithmeJeu.c, écrivez une dernière fonction jeu permettant de lancer un processus léger appelant régulièrement la fonction d'envoi de chaîne.

Faites en sorte de lancer, au niveau de la fonction principale, les fonctions messages et jeu.

3.6   Structure de données du serveur de jeu

La structure de données du serveur de jeu doit principalement permettre de gérer les joueurs qui se signalent.

Définissez cette structure de données dans le fichier entête jeu.h et écrivez sa fonction d'initialisation.

Analysez les opérations nécessaires sur les structures de données afin de déterminer celles qui nécessitent l'utilisation de sémaphores. Implantez dans votre bibliothèque libthrd les deux fonctions publiques :

void P(int) ;
void V(int) ;
Ces fonctions cachent totalement le fait que vous utilisez des verrous d'exclusion mutelle pour threads POSIX. En particulier, les verrous sont représentés par une constante.

En implantant dans les divers fichiers, les fonctions nécessaires au fonctionnement du processus Unix, prenez soin d'y ajouter les poses et levées de verrous nécessaires.

3.7   Le serveur de jeu

Vous pouvez maintenant vous attaquer à l'écriture du code concernant le serveur de jeu. Il vous est rappelé qu'il vous a été conseillé d'utiliser les fichiers suivants : L'algorithme du jeu peut se décomposer comme suit.
Phase 1 :
Boucle de récupération du type de joueur et de signalement du joueur par message. Si le joueur s'avère être un animateur et que le tableau des joueurs montre la présence de plusieurs animateurs en avertir le joueur. Cette phase ne peut pas durer moins d'une minute. Pour sortir de cette phase un unique animateur doit être présent. Le processus de l'animateur demande alors à son joueur de lancer la partie. A ce moment le processus de l'animateur passe en phase 2 et émet un message de début de partie. Sur réception de ce message les compétiteurs passent aussi en phase 2.
Phase 2/animateur :
Le processus de l'animateur demande à son joueur de lancer la manche. Il émet un message de début de manche, demande au joueur d'effectuer son geste et émet le message contenant le type du geste. Le processus de l'animateur attend un message comportant le type du geste de tous les joueurs encore en lice. Si l'attente est trop longue la manche est annulé et la partie continue. Si deux manches de suite sont annulée, la partie est annulée. Le processus de l'animateur sort de cette manche quand il s'aperçoit que le tableau des joueurs comporte moins de deux joueurs dans l'état "en lice". Il émet alors le message de fin de partie et revient dans la première phase. Sinon la phase 2 est répétée.
Phase 2/compétiteur :
Le processus du compétiteur attend le début de manche et en averti son joueur. Il se met en attente sur le geste de son joueur, le type du geste est envoyé. Si le type du geste n'est pas le bon ou que tous les autres joueurs ont déjà envoyé leur geste le processus passe en phase 3. Dans le cas contraire le processus vérifie si son joueur n'est pas le seul en lice. Auquel cas le joueur est prévenu qu'il est le gagnant et le processus repasse dans la première phase. Sinon la phase 2 est répétée.
Phase 3/compétiteur :
Le joueur est averti de son échec. Les processus des compétiteurs éliminés attendent un message de fin de partie. Ils repassent alors dans la première phase.
Il faut bien comprendre que la modification du tableau des joueurs est principalement effectuée par le processus léger de réception des messages UDP. Par exemple quand un message de signalement est reçu ce processus léger vérifie si le joueur est déjà dans le tableau et si ce n'est pas le cas l'y ajoute. Le processus léger de gestion du jeu se cantonnera donc parfois à attendre que ce tableau soit à jour. Par exemple dans la phase 2 concernant l'animateur, attendre que tous les compétiteurs aient envoyé leur type de geste consiste juste à parcourir le tableau pour vérifier qu'un champ précis est bien affecté. Si ce n'est pas le cas, le processus doit attendre un court instant et recommencer. Il est aussi possible d'éviter la boucle en utilisant des sémaphores mais dans ce cas la gestion d'un temps limite est problématique.


This document was translated from LATEX by HEVEA.