Projet IMA3 P1, 2015/2016, TD2 : Différence entre versions

De Wiki de Projets IMA
(Partie électronique)
(Arduino)
Ligne 695 : Ligne 695 :
  
 
====Arduino====
 
====Arduino====
 +
Pour l'affichage de la matrice LED nous avons utilisé un exemple trouvé sur le site de SparkFun Electronics:
 +
//Example_RGBMatrix.ino
 +
//By: Ryan Owens @ SparkFun Electronics
 +
//
 +
//modified 27 Oct 2014
 +
//by Bobby Chan @ SparkFun Electronics
 +
 +
Nous avons ensuite crée:
 +
 +
void affichage(int je[3][3])
 +
{
 +
  for (int i = 0; i < 3; i++)
 +
  {
 +
    for (int j = 0; j < 3; j++)
 +
    { if (je[i][j] == 1)
 +
      {
 +
        color_buffer[0 + i + j * 8] = BLUE;
 +
      }
 +
      else if (je[i][j] == 2)
 +
      {
 +
        color_buffer[0 + i + j * 8] = RED;
 +
      }
 +
      else if (je[i][j] == 5)
 +
      {
 +
        color_buffer[0 + i + j * 8] = TEAL;
 +
      }
 +
      else if (je[i][j] == 4)
 +
      {
 +
        color_buffer[0 + i + j * 8] = GREEN;
 +
      }
 +
      else if (je[i][j] == 3) {
 +
        color_buffer[0 + i + j * 8] = MAGENTA;
 +
      }
 +
      else if (je[i][j] == 0) {
 +
        color_buffer[0 + i + j * 8] = BLACK;
 +
      }
 +
    }
 +
  }
 +
}
 +
Qui change la valeur des buffers sur une matrice 3x3, en fonction d'une matrice donnée en paramètre de taille 3x3.
 +
 +
Il faut ensuite matrix_write(); pour observer les resultats
 +
 +
void go(int joueur)
 +
{if (joueur==2)
 +
{color_buffer[7]=GREEN;}
 +
else{color_buffer[7]=RED;}}
 +
Cette fonction met un buffer vert si le joueur 2 peut jouer sinon rouge, le buffer se trouve en dehors de la zone de jeu et donc ne gène pas l'utilisateur.
 +
 +
Il faut ensuite matrix_write(); pour observer les resultats
 +
 +
void reset(int jeu[3][3])
 +
{ joueur = 0;
 +
color_buffer[7]=RED;
 +
  for (int i = 0; i < 3; i++)
 +
  {
 +
    for (int j = 0; j < 3; j++)
 +
    { jeu[i][j] = 0;
 +
    }
 +
  }
 +
  jeu[0][0] = 1;
 +
}
 +
Cette fonction remet dispose la matrice LED comme lors d'un début de partie.
 +
On aurait pu remettre a zéro certaines variables dans cette fonction.
 +
 +
Une fois ces fonctions crées, il faut crée la fonction loop:
 +
void loop() {
 +
  go(joueur);
 +
  if (Serial.available() > 0)
 +
  { int a = (Serial.read() - 48);
 +
    if (a == 9) {
 +
      reset(jeu);
 +
    }
 +
    else {
 +
      if (joueur == 0)
 +
      { int b = (a) / 3;
 +
        int c = a - (b) * 3;
 +
        jeu[b][c] = 2;
 +
        joueur = 2;
 +
        if (jeu[0][0] == 2)
 +
        {
 +
          jeu[0][0] = 3;
 +
        }
 +
      }
 +
    }
 +
  }
 +
  else {
 +
    if (joueur == 2)
 +
    { if (digitalRead(9) == 1)
 +
      { jeu[l][h] = jeu[l][h] - 1;
 +
        h = (h + 1) % 3;
 +
        jeu[l][h]++;
 +
        delay(50);
 +
      }
 +
      else if (digitalRead(8) == 1)
 +
      {
 +
        jeu[l][h] = jeu[l][h] - 1;
 +
        l = (l + 1) % 3;
 +
        jeu[l][h]++;
 +
        delay(50);
 +
      }
 +
      else if (digitalRead(7) == 1)
 +
      {
 +
        if (jeu[l][h] == 1)
 +
        { jeu[l][h] = 4;
 +
          Serial.write((l * 3 + h) + 48);
 +
 +
          l = 0;
 +
          h = 0;
 +
          jeu[l][h] = jeu[l][h] + 1;
 +
          joueur = 0;
 +
        }
 +
      }
 +
    }
 +
  }
 +
 +
  affichage(jeu);
 +
  matrix_write();
 +
  delay(100);
 +
}
 +
Cette fonction utilise en premier lieu go() pour indiquer au joueur si il peut jouer.
 +
Ensuite il vérifie la liaison série.
 +
 +
On enlève 48 a ce que l'on a reçut, car on reçoit un char et on désire un int.
 +
 +
Ainsi on ajoutera 48 a ce qu'on enverra dans le Serial.write.
 +
 +
Si on reçoit 9, fin de partie.
 +
Sinon on vérifie qui doit jouer et on le fais jouer si on peut ou on attend.
 +
 +
Le Joueur 0 équivaut au joueur 1 pour l'html.
 +
 +
Une fois les coups effectués, on affiche ce qui en résulte et ajoute un delay pour éviter d'appuyer deux fois d'affilée sans le vouloir.
  
 
== Démonstration ==
 
== Démonstration ==
  
 
== Conclusion ==
 
== Conclusion ==

Version du 16 juin 2016 à 23:02

Projet IMA3-SC 2015/2016 : MORPION

Cahier des charges

Notre projet "système communicant" aura pour but de créer une interface connectée du "morpion". L'objectif est de permettre à un joueur interagissant sur un site internet de jouer contre une autre personne qui, elle, jouera à l'aide d'un affichage fait à partir de leds.

Nous cherchons donc à exploiter la capacité asynchrone d'un websocket afin de permettre une partie en temps réel.

Matériel

  • Une nanoboard
  • Une raspberry pi
  • Une matrice de leds 9x9 ou plus
  • Un clavier numérique ou 9 boutons-poussoirs
  • Des ordinateurs avec les logiciels de développement nécessaires

Optionnel

  • Un buzzer afin d'avertir le joueur d'un choix incohérent
  • Trois afficheurs sept segments afin d'ajouter un tableau des scores

Séance 1

Partie électronique

Le but de cette première séance était de se familiariser avec le logiciel de conception Altium Designer et de réfléchir à la façon d'aborder la conception électronique du morpion.

Cette conception se fera en plusieurs étapes :

  • gérer le positionnement d'un curseur en fonction de l'état courant de la partie
  • gérer l'affichage de la matrice en fonction des cases remplies / sélectionnées
  • permettre la réception et l'envoie de données à l'aide de la liaison série
  • traiter la remise à zéro du plateau en cas de fin de partie

Positionnement du curseur et état de la partie

Dans un premier lieu, il était nécessaire de traiter les actions entrantes. En effet, si ce n'est pas au tour du joueur "physique", l'utilisation des boutons doit être bloquée.
Ceci consiste simplement à effectuer un ET logique entre un bit symbolisant le joueur courant (si 1 alors joueur physique, si 0 alors joueur en ligne) et l'action demandée.

Schema de choix

Une fois les choix accessibles, il faut désormais les traiter. Le traitement s'effectuera à l'aide de bascules, chaque case comportera deux bascules : une bascule de sélection (la case est-elle sélectionnée ?) et une bascule permettant de savoir si la case a déjà été jouée.
Si la case a déjà été jouée, il ne faut empêcher l'utilisateur de la jouer.
Nous considérerons que le joueur en ligne n'effectuera pas de choix erroné et nous testerons seulement les choix du joueur "physique".

Schema de traitement

Les ronds verts permettent la distinction des 9 cases.
Le rond jaune correspond à la zone des sorties d'état. Plus précisément, il s'agit de deux bus regroupant les cases remplies et le lieu de la case sélectionnée.
Le rond noir correspond au choix de l'adversaire.
Le rond orange correspond à la sortie "selection_done" qui effectue un état haut lorsqu'un choix valide a été fait par l'utilisateur.
Le rond rouge met en évidence la zone permettant de traiter les décalage du bit de sélection. Ce décalage consiste à garder seulement UNE bascule à l'état haut en effectuant le passage logique d'une bascule à une autre en fonction du choix tout en appliquant un reset sur l'ancienne sélection. Le schéma de ce déplacement se trouve ci-dessous et est appliqué aux 9 cases.

Schema permettant le déplacement

Complément à la séance 1

Affichage dynamique

L'un des grands problèmes à la réalisation du morpion sur nanoboard est l'affichage dynamique. En effet, il va falloir à la fois stocker les valeurs, et stocker quel joueur a joué telle ou telle case, afin de savoir quel case s'allumera de la couleur désirée. Ceci rajouté à l'affichage dynamique de la sélection, ceci s'avère compliqué.
La tâche a alors été décomposée en deux étapes :

  • Le traitement des couleurs (Quelle couleur affilier à quelle case ?)
  • L'affichage de ces couleurs
Traitement des couleurs

Le but était de se simplifier au maximum les choses. Le premier problème est de savoir quel joueur a joué une case, pour ceci, nous nous sommes servi de l'état du plateau fourni dans le bloc TRAITEMENT. Nous savons quelles cases sont à 1 et imposons donc une couleur rouge à toutes ces cases.
De plus, nous savons qu'à chaque fois que le joueur "physique" effectue un choix valide, un front montant est appliqué sur la broche "selection_done" de TRAITEMENT. Nous avons alors re-créé 9 registres symbolisant les cases, mais cette fois ci, en fonction de l'endroit sélectionné par l'utilisateur, si ce dernier effectue un choix valide, le registre passera à 1. Le but de cette manipulation est de recenser tous les registres joués par le joueur physique.
Maintenant, nous relions ces registres aux valeurs de couleur verte afin d'additionner le vert au rouge des cases jouées et obtiendrons finalement des cases de couleur jaune pour symboliser les cases du joueur physique et des cases de couleur rouge pour le jouer en ligne.
Il ne reste qu'à relier directement la case sélectionnée (recensée par le bus Etat_selection de TRAITEMENT) à la couleur bleue.
Finalement nous avons :

  • joueur en ligne : rouge
  • joueur physique : jaune
  • sélection : magenta, cyan ou bleu
Circuit logique du traitement pré-affichage

Affichage

La matrice led employée est une matrice led à anode commune. Elle a pour avantage de permettre la sélection d'une ligne et de la couleur de chaque colonne.
Cependant, nous ne pouvons donc choisir la couleur que d'une colonne. C'est pourquoi nous allons faire varier cet affichage ligne par ligne à une fréquence élevée afin que l’œil humain ne le remarque pas. Nous pourrons donc choisir quelle couleur sera affichée sur chaque ligne.
Cet affichage ligne par ligne sera effectué via un multiplexeur donc le bus de sélection sera relié à un compteur 3bits cadencé sur une clock suffisamment rapide pour opérer une persistance rétinienne.
Dans chaque bus nous avons imposé certains bits afin que la grille soit dessinée en bleu.
Le bus multiplexé sera ensuite redirigé vers les pins de la matrice leds.

Circuit logique de l'affichage


Tests

Le bloc d'affichage a été testé et deux problèmes ont été rencontrés :

  • La sélection a été mal réfléchie
  • Un phénomène de rebond a été remarqué sur l'oscilloscope numérique
La sélection

Dans la fonction d'affichage, on retourne la sélection de la ligne en logique inversée. Le problème est que dans la version 1.0 de l'affichage, un GND avait été placé en entrée du MUX de sélection. Nous n'avions pas pensé au fait que dans ce cas la, toutes les sorties seraient en permanence au GND...
La correction a donc été de placer un VCC en entrée et d'inverser les sorties, ainsi une seul anode de ligne sera placée sur le GND.

Correction de la sortie de selection

Le schéma semble fonctionnel cependant d'autres tests restent à faire.

Le rebond

Lors du test de l'affichage, la clock d'affichage a été simulée par un bouton de la nanoboard et l'affichage a été relié sur une seule ligne.
Nous étions sensé remarquer que lorsque la ligne s'allume, si on appuie sur le bouton, elle s’éteint et enfin si nous appuyons sept fois sur le bouton, la ligne se rallume.
Rien ne se passait comme prévu, et plus précisément, il n'y avait même pas de logique dans la ré-apparition de la ligne.
Ayant déjà été confronté à des problèmes de rebonds avec des boutons poussoir, ce phénomène a été testé sur l'oscilloscope numérique.

Exemple de rebond

Nous demanderons alors aux référents quelle stratégie adopter : filtrer le signal avec un condensateur en parallèle au bouton ou bien utiliser une comparaison avec registre à décalage pour vérifier la stabilité du signal avant d'envoie du front montant.
Certes la correction du bouton n'était pas nécessaire aux tests entrepris pour l'affichage (nous aurions pu mettre une clock interne) mais ceci nous servira pour filtrer les choix effectués par l'utilisateur (haut, droite, select).


Objectifs pour la séance prochaine

Les schémas effectués restent des ébauches, il y a forcément eu des oublis. Néanmoins, comme chaque schéma se compile, le passage à l'étape de mise en place de la liaison série durant la séance 2 est envisageable. Il faudra donc créer un protocole plus concret afin de savoir qui doit jouer, quand envoyer, quand recevoir des données. Si le temps le permet, quelques programmes simples seront testés sur la nanoboard afin de vérifier le fonctionnement de l'affichage de la matrice après discutions avec les enseignants au sujet du phénomène de rebond.

Partie informatique

Dans cette première séance nous avons programmé l'arduino afin de créer le programme de morpion, pour pouvoir tester la partie informatique sans disposer de la partie électronique. Pour cela nous avons due apprendre à utiliser une matrice de LED RGB à l'aide des codes fournit par SparkFun.


Puis nous avons pu commencer à programmer le morpion:

  • le jeu se déroule dans une matrice 3x3.
  • le position du curseur est représentée par une lumière bleue, celle des pions du joueurs 1 par une lumière rouge et celle des pions du joueurs 2 par une lumière verte.
  • si le curseur est sur un pions les deux couleurs s'additionnent pour donner du magenta où du cyan.
  • le joueur se déplace à l'aide de 2 boutons poussoir,l'un permettant d'aller vers le haut, l'autre permettant d'aller à droite
    • pour éviter plusieurs déplacements d'un coup, un délais a été mis entre deux actions
  • une fois tout en haut, si le joueur presse le bouton pour aller vers le haut le joueur se retrouve en bas.
  • une fois tout à droite, si le joueur presse le bouton pour aller vers la droite le joueur se retrouve à gauche.
  • enfin un dernier bouton poussoir permet de placer un pion à l'endroit où se trouve le curseur.


Une fois les mouvements programmés il fallait régir les règles du jeu:

  • Ainsi le curseur commence toujours en haut à droite
  • Si le un joueurs arrive a créer une ligne de 3, il gagne la partie
  • Si aucun joueur n'arrive à gagner et que le terrain est remplit, la partie se termine sur un match nul
  • Une fois la partie terminée, le terrain est remis à zéros

Séance 2

Partie électronique

La matrice led ne pourra être alimentée par les différentes broches de la nanoboard sous peine de créer une chute de tension et risquer un reset du fpga.
Ainsi, le but de cet séance est de réfléchir à la conception d'un prototype de carte permettant d'alimenter la matrice led de manière externe.

Utilisation de transistors

L'utilisation de transistors en saturé paraît la meilleur chose à faire. En effet, ceci permettra de générer un gain en courant et de contrôler l'état des différentes couleurs ligne par ligne et colonne par colonne.
Pour permettra la liaison de chaque led au VCC, on utilisera un transistor PNP. Pour permettre la liaison au GND, on utilisera un NPN.

Montage d'une branche de led

Comme il s'agit de transistor à anodes communes, il suffira de 8 transistors NPN. Notre choix s'est donc porté vers le ULN2803A, qui est un ensemble de 8 paires Darlington.
L'utilisation de paires Darlington a l'avantage de permettre une génération élevée de courant. (Fort gain en courant)

Pour ce qui est des cathodes, un problème de taille se pose. Il faudrait 24 transistors PNP pour gérer les 8 colonnes * 3couleurs. Ainsi, notre choix s'est plutôt déporté vers le buffer de portes trois états 74AS240. Cette famille de buffer 3 états a la capacité de délivrer un courant du même ordre de grandeur que l'ULN2803A.

Maintenant il reste à dimensionner la résistance sur chaque branche. Après quelques tests à l'aide d'un multimètre et de la matrice, on remarque que chaque led a besoin de 2mA pour fonctionner. Donc le but est de générer un courant d'environ 2mA. Ainsi, si on impose une tension de 5.5V aux bornes du montage, R = 5.5 / 0.002 = 2700 Ohms.

Nous avons désormais toutes les informations nécessaires pour le montage d'un prototype.

Le montage

Au cours d'une séance supplémentaire, un plan a été élaboré et le prototype brasé. Voici donc le prototype achevé :

Driver de la matrice

Sur chaque sortie se trouve une résistance de 2700ohms. Le mieux aurait été de réaliser ce montage avec un réseau résistif cependant, le temps nous faisant défaut, nous avons utilisé les résistances déjà présentes en magasin.
Sur chaque entrée se trouver une broche qui sera reliée à la nanoboard.
Tous les buffers ont leur GND et VCC reliés au générateur 5.5V. Les paires Darlington sont reliées à la même masse.

Il ne restait plus qu'à relier la matrice led. Cette liaison parallèle a été réalisée en recyclant un vieux BUS d'ordinateur fixe. Finalement, voilà le montage final :

Montage final de la partie électronique

Partie informatique

Arduino

Lors de cette séance nous avons pu finir le programme arduino, pour pouvoir jouer à deux sur une la carte. Cette partie a été crée pour pouvoir tester les différentes fonctions, et sera modifier lors de prochaines séances. La matrice LED affiche des clignotement irrégulier de couleur rouge, mais cela ne gêne pas pour jouer.

Raspberry

Nous avons pu enfin commencer notre travail sur la raspberry. Apres avoir branché la raspberry en série à notre pc, nous avons pu la configurer afin qu'elle fonctionne avec le réseau de l'ecole. Pour cela nous avons mis une adresse IP du réseau INSECURE de l'école sur la Raspberry, celle de notre raspberry est :172.26.79.12. C'est à partir de cette adresse que nous allons acceder aux pages html du projet. Pour effectuer cette configuration nous avons remplacé les lignes correspondant à la configuration de la carte Ethernet dans le fichier /etc/network/interfaces par les lignes

 auto eth0
 iface eth0 inet static
   address 172.26.79.12
   netmask 255.255.240.0
   gateway 172.26.79.254

Et afin d'indiquer le serveur DNS dans /etc/resolv.conf nous avons rajouté:

 nameserver 193.48.57.34 

Une fois la raspberry configurée, nous pouvons accéder à notre raspberry sans liaisons série grâce à un ssh. Afin d'effectuer notre projet nous avons besoin de deux bibliothèques:

  • jquery.js que l'ont a telecharger sur http://jquery.com/ afin de faciliter la programmation en javascript
  • une bibliothèque C de WebSocket trouvé sur http://libwebsockets.org afin de pouvoir effectuer notre programme en C utilisant le Websocket

Notre raspberry est maintenant prête pour notre projet, nous avons pu commencer notre fichier morpion.html qui permet de joueur au morpion à deux sur le même ordinateur, ce qui permet de tester les differentes fonctions utilisés pour la suite du projet.

Séance 3

Partie électronique

Le montage réalisé a été testé. Quelques soudures ont été refaites et un composant ne fonctionnait pas. Heureusement, nous avions placé des socles afin de pouvoir enlever/placer les buffers facilement.

Cette mise en évidence de composant défaillant a pris beaucoup de temps, c'est pourquoi tout ce qui suit a été réalisé sur plusieurs séances supplémentaires.

La première chose effectuée fut le branchement de la carte à la nanoboard. Pour cela, nous avons utilisé un plan créé au préalable et un schematic de vérification.
Ce schematic consistait à afficher colonne par colonne et couleur par couleur chaque led, ainsi, si un branchement était mauvais, nous pouvions le voir directement.

Le montage finalisé, nous pouvions commencer les tests des fonctions créées en première séance. Sur le principe, ces schematics marchaient plutôt bien mais comme nous souhaitions générer des simulations de données venant de la liaison série, nous n'avions pas assez de boutons pour utiliser ces premières schematics.

Ainsi, le travail de traitement a dû être refait.

La seletion

L'objectif est de créer un circuit numérique de sélection à l'aide de seulement deux boutons.
Le choix s'est alors porté sur un compteur allant jusque 8. Ainsi, il y avait 9 possibilités de valeur et chaque valeur correspondra a une case.
Le compteur devra s'incrémenter sur l'appuie d'un bouton de balayage.

Selection

La sélection du joueur physique doit être contrôlée afin que seul un choix valide soit accepté. Ainsi, un schematic de vérification a été créé.

Pour savoir quelles cases ont déjà été jouées, nous avons décidé d'effectuer un OU logique entre les cases jouées par le joueur physique et les cases jouées par le joueur en ligne. Une fois le bus d'état global créé, il suffit d'effectuer un ET logique avec le choix de notre joueur physique. Si le choix se porte sur une case qui n'a jamais été jouée, le résultat de l'opération logique sera un bus nul. Nous comparons donc ce bus avec un bus nul.
Finalement, le choix est valide si le comparateur sort une égalité ET le bouton de sélection a été appuyé.

Valider

L'affichage

Les schematics d'affichage sont restés les mêmes qu'à la séance 1, mis à part quelques corrections tel l'inversion des sorties d'affichage car nous travaillons en logique inversée sur les portes 3 états.
Le pré-affichage s'occupe toujours du stockage des cases cochées par le joueur physique si il est le joueur courant.
Afin d'assurer une persistance rétinienne, on devra relier l'affichage à une horloge d'au moins 3KHz.

Pre-affichage
Affichage

Simulation de la liaison série

Le but est de gérer des données semblables à ce que la raspberry pourrait nous envoyer. A savoir 1 bit de fin de partie, 1 bit de réception de donnée et 4 bits de choix de case.

Conversion de l'adresse choisie

Il faut faire correspondre les 4 bits reçus à un bus de 9bits, chaque bit correspondant à une case. Ceci revient à faire un traitement similaire à la sélection du joueur physique...

Conversion de l'adresse

Stockage

Maintenant que nous savons convertir une adresse, on doit pouvoir mettre à jour les cases jouées par le joueur en ligne sur chaque réception de donnée.
Pour effectuer ceci, on utilise 9 bascules pour stocker les choix dans différentes cases. Aucun traitement de choix valide n'est à faire car la raspberry s'en occupe déjà de son côté.

Stockage des cases de l'adversaire

Propre à la simulation

Pour générer l'adresse, on utilise le même principe de sélection que celui du joueur physique.
Pour avoir un aspect visuel de cette sélection, on utilise un MUX qui, en fonction du joueur courant, afficher la sélection de tel ou tel joueur.

MUX de la sélection

Pour ce qui est du bit de fin de parti, on le symbolise par un bouton.

Phénomène de rebond

Pour gérer le rebond des boutons poussoirs, la solution a été numérique. Un registre à décalage a été créé, cadencé sur une clock, il décale sur chaque front d'horloge la valeur courante d'un bouton. En utilisant un comparateur, on obtient alors un front montant seulement lorsque le bouton est dans un état stable (c'est à dire lorsque tous les bits ont la même valeurs)

Anti-rebond

TOPLEVEL

Le schéma TOPLEVEL est alors le suivant :

TOPLEVEL

Ainsi, si nous avions eu le bloc de liaison série bidirectionnel, nous aurions simplement eu à :

  • Remplacer la génération d'adresse par les 4bits de poids faible du message de 8bits reçu.
  • Remplacer le bouton de reset par le bit de fin de partie.
  • Remplacer le bit de choix de l'adversaire par celui de réception validée.
  • Relier le bit d'émission à la sortie de "choix valide".
  • Relier la sélection du joueur physique aux données à transmettre.
  • Enlever le multiplexage de la sélection.

Réflexion

La partie électronique de ce projet a donc été menée à bout. Cependant, après coup, on se rend compte que la création d'un prototype aurait été meilleure en imprimant un PCB plutôt qu'en utilisant une piste à bandes.
Nous aurions également pu éviter une configuration en parallèle du driver. Ainsi, à l'aide de registres à décalages alimentés en externe, la lisibilité du circuit aurait été meilleure.
On peut néanmoins se satisfaire de la réalisation numérique du circuit car elle exploite toutes les possibilités qu'offrent la conception via schematics à l'aide d'Altium designer à savoir :

  • La création d'une bibliothèque
  • La création de composants
  • L'utilisation de composants configurables avec édition de pins

D'ailleurs, on comprend d'autant plus l'intérêt du langage VHDL après la création d'autant de schematics pour un projet de cette envergure.

Partie informatique

Nous avons commencé cette séance en continuant le morpion.html Dans un premier temps nous avons crée "l'espace de jeu" en utilisant du code trouvé sur internet :

<!DOCTYPE html>
 <head>
  <meta charset="utf-8">
  <style type="text/css">
.table{
width : 330px;
height : 330px;
border : solid 3px black;
 border-collapse: collapse;
}
.td{
border : solid 3px black;
width : 33%;
height : 33%;
}
</style>
<title> Jeu morpion</title>
</head>

et

<body>
  h1>Morpion</h1>
table class="table" align="center">
   <tr>
      <td class="td" onclick="placerSigne(0)" id="0"> </td>
      <td class="td" onclick="placerSigne(1)" id="1"> </td>
      <td class="td" onclick="placerSigne(2)" id="2"> </td>
   </tr>
   <tr>
      <td class="td" onclick="placerSigne(3)" id="3"> </td>
      <td class="td" onclick="placerSigne(4)" id="4"> </td>
      <td class="td" onclick="placerSigne(5)" id="5"> </td>
   </tr>
   <tr>
      <td class="td" onclick="placerSigne(6)" id="6"> </td>
      <td class="td" onclick="placerSigne(7)" id="7"> </td>
      <td class="td" onclick="placerSigne(8)" id="8"> </td>
   </tr>
</table>
table style="border : solid 3px #3399FF;" align="center">
<tr>
<td style="border : solid 1px #3399FF;">Joueur 1</td>
<td style="border : solid 1px #3399FF;">Joueur 2</td>
</tr>
<tr>
<td style="border : solid 1px #3399FF;" id="scre1">0
</td>
<td style="border : solid 1px #3399FF;" id="scre2">0</td>
</tr>
</table>
 </body>
</html>

Le code ci dessus n'est pas totalement bon, puisque nous avons du enlever des "<" afin d'afficher le code et non ce qu'il en résulte.

L'espacement entre deux lignes peut changer en fonction de la taille de ce qui est mis dedans. Si on remplis de case blanche ("vide.png") de taille 100*100 on obtient: Morpion1.png

A chaque clique sur une case la fonction placerSigne(n) va s'effectuer avec n le numéros de la case sélectionnée.

function placerSigne(idCase)
{ 
        if(morpion[idCase] == 0)
        {coup++;
        morpion[idCase]=joueur;
        if (joueur==1)
                {
 	  	 joueur=2;
                document.getElementById(idCase).innerHTML = '<img src="croix.gif" alt="image !" />';

                }
	else    {
	   	 joueur=1;
                document.getElementById(idCase).innerHTML = '<img src="rond.gif" alt="image !" />';

                }
	if (coup>4)
		{if (gagner(idCase))
			{if (joueur==1)
				{score2++;
				alert("joueur 2 gagne ");
				finpartie();
				}
			else{score1++;
				alert("joueur 1 gagne ");
				finpartie();
				}
			
			}
		else {if (egalite()==0)
			{alert("egalite");
			finpartie();}
		     }
		}
        }
        else{alert("pas le droit");}
}

Cette fonction place une image 100x100 de rond ou de croix dans la case sélectionnée en fonction du joueur qui doit jouer.

  • Elle calcule les scores de chaque joueurs, et appelle gagner et egalite pour savoir si on est dans une fin de partie.
  • Et elle alerte le joueur si la case sélectionnée ne peut pas être jouée.

Pour cela elle utilise le tableau morpion de taille 9 remplit de 0 à l'origine, et modifie la case sélectionnée en y mettant 1 ou 2 en fonction du joueur qui a joué. La variable coup permet de déterminer le nombre de coup jouer, ce qui permet de ne pas verifier si une partie une gagnée tant que coup n'est pas supérieur à 4, puisqu'il faut au minimum 5 coup pour gagner.

function finpartie()
{for (var j=0;j<9;j++)
	{ document.getElementById(j).innerHTML = '<img src="vide.png" alt="image !" />';
	morpion[j]=0;
	}
}

La fonction finpartie() met les variables morpion et coup à zéro, et place des images blanches dans toutes les cases. Elle est appelé que si il y une victoire ou une égalité. Dans morpionsc.html qui est le fichier html final, joueur est remis à 1 dans cette fonction. Dans le fichier morpion.html cela n'etais pas necessaire et permettait en plus au joueur perdant de commencer. Mais pour simplifier la suite nous avons préféré définir que toujours le même joueur commencerais.


function gagner(idCase)
{switch(idCase)
        {
case 0:
        return (diagonal1()||ligne1()||colonne1());
        break;
case 1:
        return (ligne1()||colonne2());
        break;
case 2:
        return (diagonal2()||ligne1()||colonne3());
        break;
case 3:
        return (ligne2()||colonne1());
        break; 

case 4:
        return (diagonal1()||diagonal2()||ligne2()||colonne2());
        break;
case 5:
        return (ligne2()||colonne3());
        break;
case 6:
        return (diagonal2()||ligne3()||colonne1());
        break;
case 7:
        return (ligne3()||colonne2());
        break;
case 8:
        return (diagonal1()||ligne3()||colonne3());
        break;
        }
}

La fonction gagner appelle des fonctions diagonal,ligne,colonne qui retourne 1 si il y'a 3 même symboles sur leur domaines. Gagner teste donc si il y a une suite de 3 symboles sur la ligne, colonne et les diagonales passant par la case qui a été joué.

function egalite()
{var cp=0;
for (var i=0;i < 9;i++)
        {if (morpion[i]==0)
		{cp++;}
        }
return cp;
}

La fonction egalité teste si toutes les cases ont été joué. On aurait pu utiliser la variable coup, si coup était égal à 9 alors égalité, mais la fonction egalité permet de vérifier si il n'y a pas eu de problème lors de la modification de la matrice.

Avec morpion.html nous avons pu apprendre le javascript, et poser les bases de notre fichier html final.

Complément à la séance 3

HTML

Pour le HTML/Javascript on garde les mêmes fonctions gagner et egalite. On va ajouter lors de l'initialisation

window.WebSocket=(window.WebSocket||window.MozWebSocket);

var websocket=new WebSocket('ws://172.26.79.12:9000','myprotocol');

websocket.onopen=function(){ $('h1').css('color','green'); };

websocket.onerror=function(){ $('h1').css('color','red'); };

On modifie placerSigne:

  • si ce n'est pas au tour du joueur 1 de jouer on lui envoie une alerte.
  • si il y a une fin de partie on envoie 9 grâce a sendMessage.
  • si il n'y a pas de fin de partie on envoie la case jouer toujours grâce à sendMessage:
function sendMessage(data){
websocket.send(data);
}

Pour finpartie on rajoute juste

joueur=1;

Pour faire jouer le second joueur il faut recevoir un message du websocket ainsi :

websocket.onmessage=function(message)

sera la fonction dans laquelle on fera jouer le joueur 2.

Le websocket envoie simplement la case en char* ainsi:

websocket.onmessage=function(message){
idCase=parseInt((message.data)-30);
console.log(idCase);

if(morpion[idCase] == 0)
                {coup++;
                 morpion[idCase]=2;
                 joueur=1;
 		document.getElementById(idCase).innerHTML = '<img src="rond.gif" alt="image !" />';
 		 if (coup>4)
			 {if (gagner(idCase))
                               {score2++; 
                                        alert("joueur 2 gagne ");
                                        finpartie();
					sendMessage(9);
                               }
 

                          else {if (egalite()==0)
                                        {alert("egalite");
                                        finpartie();
					sendMessage(9);

                                        }
				}
			
			}
		}

}

Console.log permet d'afficher dans le debug, la valeur traitée. On envoie toujours 9 si la partie se termine, sinon on effectue la même chose que dans placerSigne de morpion.html

Fichier C

Il faut maintenant créer la passerelle entre l'arduino et le fichier html. Pour cela notre fichier doit utiliser des fonctions de websocket mais aussi de serial.c Les fonctions de serial.c sont directement intégrées dans notre fichier, il n'y a donc pas besoin de lui faire appel pour compiler.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/file.h>
#include <linux/serial.h>
#include <libwebsockets.h>

#define MAX_FRAME_SIZE  1024
#define WAIT_DELAY      50
#define MAX_MESSAGE     2 

#define SERIAL_READ 0
#define SERIAL_WRITE 1
#define SERIAL_BOTH 2

#define SERIAL_DEVICE "/dev/ttyUSB0"

int sd;
 

//
// Open serial port device
//
int serialOpen(char *device,int mode){
int flags=(mode==SERIAL_READ?O_RDONLY:(mode==SERIAL_WRITE?O_WRONLY:O_RDWR));
int fd=open(device,flags|O_NOCTTY|O_NONBLOCK); 
if(fd<0){ perror(device); exit(-1); }
return fd;
}
 
//
// Serial port configuration
//
void serialConfig(int fd,int speed){
struct termios new;
bzero(&new,sizeof(new));
new.c_cflag=CLOCAL|CREAD|speed|CS8;
new.c_iflag=0;
new.c_oflag=0;
new.c_lflag=0;     /* set input mode (non-canonical, no echo,...) */
new.c_cc[VTIME]=0; /* inter-character timer unused */
new.c_cc[VMIN]=1;  /* blocking read until 1 char received */
if(tcsetattr(fd,TCSANOW,&new)<0){ perror("serialInit.tcsetattr"); exit(-1); }
}
 
//
// Serial port termination
//
void serialClose(int fd){
close(fd);
}

static int callback_http(
  struct libwebsocket_context *this,
  struct libwebsocket *wsi,enum libwebsocket_callback_reasons reason,
  void *user,void *in,size_t len)
{
return 0;
}

 static struct libwebsocket_protocols protocols[] = {
   {
     "http-only",   // name
     callback_http, // callback
     0,             // data size
     0              // maximum frame size
   },
   {"myprotocol",callback_my,0,MAX_FRAME_SIZE},
   {NULL,NULL,0,0}
   };
 

Ci-dessus se trouve le code se trouvant dans serial.c afin de créer serialOpen, serialClose et serialConfig, ainsi que l'initialisation de variables utilisées. Mais aussi la fonction callback_http(),la structure libwebsocket_protocols, et l'initialisation de variables.

int main(void) {
int port=9000;
struct lws_context_creation_info info;
memset(&info,0,sizeof info);
info.port=port;
info.protocols=protocols;
info.gid=-1;
info.uid=-1;
struct libwebsocket_context *context=libwebsocket_create_context(&info);
if(context==NULL){
  fprintf(stderr, "libwebsocket init failed\n");
  return -1;
  }
printf("starting server...\n");

  
sd=serialOpen(SERIAL_DEVICE,SERIAL_BOTH);
serialConfig(sd,B9600);

 
while(1){
  libwebsocket_service(context,WAIT_DELAY);
  }
    

serialClose(sd); 

 
libwebsocket_context_destroy(context);
return 0;
}

Le main permet d'initialiser une liaison série de 9600 Baud et de lancer une boucle infini dans laquelle la passerelle entre arduino et html va s'effectuer.

static int callback_my(
  struct libwebsocket_context * this,
  struct libwebsocket *wsi,enum libwebsocket_callback_reasons reason,
  void *user,void *in,size_t len)
{
static char message[2+LWS_SEND_BUFFER_PRE_PADDING+LWS_SEND_BUFFER_POST_PADDING];
static char valeur;
switch(reason){
  case LWS_CALLBACK_ESTABLISHED:
    printf("connection established\n");
                // Declenchement d'un prochain envoi au navigateur
    break;
  case LWS_CALLBACK_RECEIVE:
                // Ici sont traites les messages envoyes par le navigateur
//printf("received data: %s\n",(char *)in);
    if(write(sd,in,sizeof(char))!=1){ perror("main.write"); exit(-1); }
printf("writen data: %s\n",(char *)in);
                // Declenchement d'un prochain envoi au navigateur
    libwebsocket_callback_on_writable(this,wsi);
    break;
  case LWS_CALLBACK_SERVER_WRITEABLE:
                // Ici sont envoyes les messages au navigateur

if (read(sd,&valeur,sizeof(char))==1)
{ 
//printf("write data: %c\n",valeur);
      char *out=message+LWS_SEND_BUFFER_PRE_PADDING;
       sprintf(out,"%02x",valeur);
//printf("write data: %s\n",(char *)out);

    libwebsocket_write(wsi,(unsigned char *)out,MAX_MESSAGE,LWS_WRITE_TEXT);
}
else{libwebsocket_callback_on_writable(this,wsi);
}
    break;
  default:
    break;
       	  }   
return 0;
}

La fonction ci-dessus est celle qui definie la passerelle. Si elle établit une liaison, elle imprime "connection established". Une fois la connection établit on se retrouve dans le cas LWS_CALLBACK_RECEIVE si la page html envoie une valeur. Dans ce cas la valeur pointée par in est écrite dans sd(liaison serie), puis on utilise :

libwebsocket_callback_on_writable(this,wsi);

Qui permet de passer en mode réception de message par la liaison série, qui est definit ici par le cas:

case LWS_CALLBACK_SERVER_WRITEABLE:

Dans ce cas on lit la valeur dans la liaison serie et on la met dans la variable valeur. Si il n'y a rien à lire pour le moment on utilise a nouveau:4

libwebsocket_callback_on_writable(this,wsi);

Afin de rester dans le même case. Si la valeur a été lu on crée out:

 char *out=message+LWS_SEND_BUFFER_PRE_PADDING;

avec message :

static char message[2+LWS_SEND_BUFFER_PRE_PADDING+LWS_SEND_BUFFER_POST_PADDING];

out va etre du bon type et de la bonne taille pour pouvoir être envoyer a l'html. On modife donc la valeur de out grâce à la valeur lue:

sprintf(out,"%02x",valeur);

Puis on l'envoie:

libwebsocket_write(wsi,(unsigned char *)out,MAX_MESSAGE,LWS_WRITE_TEXT);


Arduino

Pour l'affichage de la matrice LED nous avons utilisé un exemple trouvé sur le site de SparkFun Electronics:

//Example_RGBMatrix.ino
//By: Ryan Owens @ SparkFun Electronics
//
//modified 27 Oct 2014
//by Bobby Chan @ SparkFun Electronics

Nous avons ensuite crée:

void affichage(int je[3][3])
{
  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 3; j++)
    { if (je[i][j] == 1)
      {
        color_buffer[0 + i + j * 8] = BLUE;
      }
      else if (je[i][j] == 2)
      {
        color_buffer[0 + i + j * 8] = RED;
      }
      else if (je[i][j] == 5)
      {
        color_buffer[0 + i + j * 8] = TEAL;
      }
      else if (je[i][j] == 4)
      {
        color_buffer[0 + i + j * 8] = GREEN;
      }
      else if (je[i][j] == 3) {
        color_buffer[0 + i + j * 8] = MAGENTA;
      }
      else if (je[i][j] == 0) {
        color_buffer[0 + i + j * 8] = BLACK;
      }
    }
  }
}

Qui change la valeur des buffers sur une matrice 3x3, en fonction d'une matrice donnée en paramètre de taille 3x3.

Il faut ensuite matrix_write(); pour observer les resultats

void go(int joueur)
{if (joueur==2)
{color_buffer[7]=GREEN;}
else{color_buffer[7]=RED;}}

Cette fonction met un buffer vert si le joueur 2 peut jouer sinon rouge, le buffer se trouve en dehors de la zone de jeu et donc ne gène pas l'utilisateur.

Il faut ensuite matrix_write(); pour observer les resultats

void reset(int jeu[3][3])
{ joueur = 0;
color_buffer[7]=RED;
  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 3; j++)
    { jeu[i][j] = 0;
    }
  }
  jeu[0][0] = 1;
}

Cette fonction remet dispose la matrice LED comme lors d'un début de partie. On aurait pu remettre a zéro certaines variables dans cette fonction.

Une fois ces fonctions crées, il faut crée la fonction loop:

void loop() {
  go(joueur);
  if (Serial.available() > 0)
  { int a = (Serial.read() - 48);
    if (a == 9) {
      reset(jeu);
    }
    else {
      if (joueur == 0)
      { int b = (a) / 3;
        int c = a - (b) * 3;
        jeu[b][c] = 2;
        joueur = 2;
        if (jeu[0][0] == 2)
        {
          jeu[0][0] = 3;
        }
      }
    }
  }
  else {
    if (joueur == 2)
    { if (digitalRead(9) == 1)
      { jeu[l][h] = jeu[l][h] - 1;
        h = (h + 1) % 3;
        jeu[l][h]++;
        delay(50);
      }
      else if (digitalRead(8) == 1)
      {
        jeu[l][h] = jeu[l][h] - 1;
        l = (l + 1) % 3;
        jeu[l][h]++;
        delay(50);
      }
      else if (digitalRead(7) == 1)
      {
        if (jeu[l][h] == 1)
        { jeu[l][h] = 4;
          Serial.write((l * 3 + h) + 48);

          l = 0;
          h = 0;
          jeu[l][h] = jeu[l][h] + 1;
          joueur = 0;
        }
      }
    }
  }

  affichage(jeu);
  matrix_write();
  delay(100);
}

Cette fonction utilise en premier lieu go() pour indiquer au joueur si il peut jouer. Ensuite il vérifie la liaison série.

On enlève 48 a ce que l'on a reçut, car on reçoit un char et on désire un int.

Ainsi on ajoutera 48 a ce qu'on enverra dans le Serial.write.

Si on reçoit 9, fin de partie. Sinon on vérifie qui doit jouer et on le fais jouer si on peut ou on attend.

Le Joueur 0 équivaut au joueur 1 pour l'html.

Une fois les coups effectués, on affiche ce qui en résulte et ajoute un delay pour éviter d'appuyer deux fois d'affilée sans le vouloir.

Démonstration

Conclusion