edge lab LIVE! starts Tuesday in Munich. Grab a workshop seat and take home the board you build on. Code FRIEND-HACKSTER saves 20%. () Learn more!
BYSGR
Published

Pocket Chess: Bluetooth-Controlled Chess on TFT

An embedded chess board on an ILI9341 TFT screen, where you select and move pieces from your phone via Bluetooth.

AdvancedShowcase (no instructions)2 hours18
Pocket Chess: Bluetooth-Controlled Chess on TFT

Things used in this project

Hardware components

2.8 TFT Touch Shield V2.0
Seeed Studio 2.8 TFT Touch Shield V2.0
×1
HC-05 Bluetooth Module
HC-05 Bluetooth Module
×1
STM32 Nucleo-64 Board
STMicroelectronics STM32 Nucleo-64 Board
×1

Software apps and online services

VS Code
Microsoft VS Code

Story

Read more

Schematics

Cablage

Code

main.cpp

C/C++
// =============================================================================
//  Jeu d'échecs sur écran TFT ILI9341, piloté en Bluetooth (HC-05)
//  Carte : Nucleo-64 (core STM32duino)
//
//  Principe :
//   - le plateau est stocké dans un tableau 8x8 de caractères (lettres = pièces)
//   - le téléphone envoie des lettres (F, B, L, R, C) via le HC-05
//   - un curseur jaune se déplace sur le plateau, "C" sélectionne / déplace
//   - aucune règle d'échecs n'est vérifiée pour l'instant
// =============================================================================

#include <Arduino.h>
#include <Wire.h>             // Inclus mais pas utilisé pour l'instant (I2C)
#include <Adafruit_GFX.h>     // Fonctions graphiques de base (rectangles, texte)
#include <Adafruit_ILI9341.h> // Pilote de l'écran ILI9341

// ===== Communication Bluetooth HC-05 =====
// Port série matériel dédié au HC-05 : USART1
// PA10 = RX de la Nucleo (relié au TX du HC-05)  -> D2 sur le connecteur Arduino
// PA9  = TX de la Nucleo (relié au RX du HC-05)  -> D8 sur le connecteur Arduino
HardwareSerial BT(PA10, PA9); // (RX, TX)

// ===== Configuration écran TFT ILI9341 =====
// Broches de contrôle (le SPI utilise D11 = MOSI et D13 = SCK automatiquement)
#define TFT_CS  10   // Chip Select : active l'écran sur le bus SPI
#define TFT_DC  7    // Data/Command : indique si l'octet envoyé est une commande ou une donnée
#define TFT_RST 9    // Reset de l'écran

// Création de l'objet écran
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);

// ===== Configuration échiquier =====
const int Taille_case = 28;   // Côté d'une case en pixels (8 x 28 = 224 px)
const int Marge = 8;          // Marge autour du plateau (240 - 224 = 16 -> 8 de chaque côté)

// Couleurs au format RGB565 (16 bits)
const uint16_t COL_BLANC     = 0xEF5D;          // Cases claires
const uint16_t COL_NOIR      = 0x4A49;          // Cases foncées
const uint16_t COL_CURSEUR   = ILI9341_YELLOW;  // Contour du curseur
const uint16_t COL_SELECTION = ILI9341_RED;     // Contour de la pièce sélectionnée

// ===== Touches du gamepad =====
// Caractères envoyés par l'application du téléphone
const char TOUCHE_HAUT    = 'F';  // Flèche haut (Forward)
const char TOUCHE_BAS     = 'B';  // Flèche bas (Backward)
const char TOUCHE_GAUCHE  = 'L';  // Flèche gauche (Left)
const char TOUCHE_DROITE  = 'R';  // Flèche droite (Right)
const char TOUCHE_ROND    = 'C';  // Bouton rond (Cercle) = valider

// ===== Position du curseur =====
int curLigne = 6;   // Départ : sur le pion devant le roi blanc (ligne 6, colonne 4)
int curCol   = 4;

// ===== Pièce sélectionnée =====
bool selActive = false;  // true si une pièce est actuellement sélectionnée
int  selLigne  = 0;      // Position de la pièce sélectionnée
int  selCol    = 0;

// ===== Plateau d'échecs =====
// Majuscules = pièces blanches, minuscules = pièces noires, ' ' = case vide
// R/r roi, D/d dame, T/t tour, F/f fou, C/c cavalier, P/p pion
// Ligne 0 en haut : pièces noires
char plateau[8][8] = {
  {'t','c','f','d','r','f','c','t'},
  {'p','p','p','p','p','p','p','p'},
  {' ',' ',' ',' ',' ',' ',' ',' '},
  {' ',' ',' ',' ',' ',' ',' ',' '},
  {' ',' ',' ',' ',' ',' ',' ',' '},
  {' ',' ',' ',' ',' ',' ',' ',' '},
  {'P','P','P','P','P','P','P','P'},
  {'T','C','F','D','R','F','C','T'}
};

// ===== Outils =====

// Renvoie true si la pièce est blanche (lettre majuscule)
bool estBlanc(char p)
{
  return (p >= 'A' && p <= 'Z');
}

// ===== Dessiner un contour de 2 pixels d'épaisseur =====
// decalage : nombre de pixels de retrait vers l'intérieur de la case
void dessinerContour(int ligne, int col, uint16_t couleur, int decalage)
{
  // Coin haut-gauche de la case, plus le décalage
  int x = Marge + col * Taille_case + decalage;
  int y = Marge + ligne * Taille_case + decalage;
  // Taille du carré, réduite de chaque côté par le décalage
  int t = Taille_case - 2 * decalage;

  tft.drawRect(x, y, t, t, couleur);                  // Trait extérieur
  tft.drawRect(x + 1, y + 1, t - 2, t - 2, couleur);  // Trait intérieur (2 px au total)
}

// ===== Dessiner une case (fond + pièce + sélection + curseur) =====
// Redessine tout depuis le fond, ce qui efface proprement l'ancien contenu
void dessinerCase(int ligne, int col)
{
  int x = Marge + col * Taille_case;
  int y = Marge + ligne * Taille_case;

  // Fond : (ligne + col) impair = case foncée, pair = case claire
  uint16_t couleur = ((ligne + col) & 1) ? COL_NOIR : COL_BLANC;
  tft.fillRect(x, y, Taille_case, Taille_case, couleur);

  // Pièce : lettre en vert pour les blancs, en bleu pour les noirs
  char p = plateau[ligne][col];
  if (p != ' ')
  {
    tft.setTextSize(3);  // Lettre de 18 x 24 px
    tft.setTextColor(estBlanc(p) ? ILI9341_GREEN : ILI9341_BLUE);
    tft.setCursor(x + 5, y + 2);  // Centrage approximatif dans la case
    tft.print(p);
  }

  // Contour de sélection (rouge, sur le bord extérieur de la case)
  bool estSelection = (selActive && ligne == selLigne && col == selCol);
  if (estSelection)
    dessinerContour(ligne, col, COL_SELECTION, 0);

  // Curseur (jaune) : décalé de 2 px vers l'intérieur s'il est sur la case
  // sélectionnée, pour que les deux contours restent visibles ensemble
  if (ligne == curLigne && col == curCol)
    dessinerContour(ligne, col, COL_CURSEUR, estSelection ? 2 : 0);
}

// ===== Dessiner l'échiquier complet =====
void chessboard()
{
  for (int ligne = 0; ligne < 8; ligne++)
    for (int col = 0; col < 8; col++)
      dessinerCase(ligne, col);
}

// ===== Actualiser l'affichage =====
// Simple raccourci : redessine tout le plateau
void actualiserEchiquier()
{
  chessboard();
}

// ===== Déplacer le curseur =====
// dLigne / dCol valent -1, 0 ou +1
void deplacerCurseur(int dLigne, int dCol)
{
  int nLigne = curLigne + dLigne;
  int nCol   = curCol + dCol;

  // Ne rien faire si on sort du plateau
  if (nLigne < 0 || nLigne > 7 || nCol < 0 || nCol > 7)
    return;

  // Mémoriser l'ancienne position pour pouvoir effacer son contour
  int ancienneLigne = curLigne;
  int ancienneCol   = curCol;

  curLigne = nLigne;
  curCol   = nCol;

  // On ne redessine que 2 cases (plus rapide que tout le plateau)
  dessinerCase(ancienneLigne, ancienneCol);
  dessinerCase(curLigne, curCol);
}

// ===== Validation (bouton ROND) =====
// Selon la situation : sélectionne, annule, change de sélection ou déplace
void valider()
{
  char pieceCurseur = plateau[curLigne][curCol];  // Contenu de la case sous le curseur

  // --- Cas 1 : aucune pièce sélectionnée -> on tente de sélectionner ---
  if (!selActive)
  {
    // On ne peut pas sélectionner une case vide
    if (pieceCurseur == ' ')
    {
      Serial.println("Case vide : rien a selectionner");
      return;
    }

    // Mémoriser la sélection et afficher le contour rouge
    selActive = true;
    selLigne  = curLigne;
    selCol    = curCol;
    dessinerCase(curLigne, curCol);

    Serial.print("Piece selectionnee : ");
    Serial.println(pieceCurseur);
    return;
  }

  // --- Une pièce est déjà sélectionnée ---

  // Cas 2 : on revalide la même case -> on annule la sélection
  if (curLigne == selLigne && curCol == selCol)
  {
    selActive = false;
    dessinerCase(curLigne, curCol);  // Efface le contour rouge
    Serial.println("Selection annulee");
    return;
  }

  char pieceSel = plateau[selLigne][selCol];  // Pièce actuellement sélectionnée

  // Cas 3 : la case visée contient une pièce de la même couleur
  // -> on ne capture pas, on change simplement de sélection
  if (pieceCurseur != ' ' && estBlanc(pieceCurseur) == estBlanc(pieceSel))
  {
    int ancienneLigne = selLigne;
    int ancienneCol   = selCol;

    selLigne = curLigne;
    selCol   = curCol;

    dessinerCase(ancienneLigne, ancienneCol);  // Retire le contour de l'ancienne
    dessinerCase(selLigne, selCol);            // Affiche le contour de la nouvelle

    Serial.print("Nouvelle selection : ");
    Serial.println(pieceCurseur);
    return;
  }

  // Cas 4 : case vide ou pièce adverse -> déplacement (ou capture)
  // Aucune règle d'échecs n'est vérifiée : tout déplacement est accepté
  plateau[curLigne][curCol] = pieceSel;  // La pièce arrive (écrase l'éventuelle pièce capturée)
  plateau[selLigne][selCol] = ' ';       // La case de départ devient vide

  int departLigne = selLigne;
  int departCol   = selCol;
  selActive = false;  // Plus de sélection

  // Redessiner uniquement les deux cases modifiées
  dessinerCase(departLigne, departCol);
  dessinerCase(curLigne, curCol);

  // Trace dans le moniteur série
  Serial.print("Deplacement : ");
  Serial.print(pieceSel);
  Serial.print(" (");
  Serial.print(departLigne);
  Serial.print(",");
  Serial.print(departCol);
  Serial.print(") -> (");
  Serial.print(curLigne);
  Serial.print(",");
  Serial.print(curCol);
  Serial.println(")");
}

// ===== Traitement des commandes Bluetooth =====
// Associe chaque caractère reçu à une action
void Commande(char command)
{
  switch (command)
  {
    case TOUCHE_HAUT:
      Serial.println("Fleche HAUT");
      deplacerCurseur(-1, 0);   // Une ligne plus haut
      break;

    case TOUCHE_BAS:
      Serial.println("Fleche BAS");
      deplacerCurseur(1, 0);    // Une ligne plus bas
      break;

    case TOUCHE_GAUCHE:
      Serial.println("Fleche GAUCHE");
      deplacerCurseur(0, -1);   // Une colonne à gauche
      break;

    case TOUCHE_DROITE:
      Serial.println("Fleche DROITE");
      deplacerCurseur(0, 1);    // Une colonne à droite
      break;

    case TOUCHE_ROND:
      Serial.println("Validation (ROND)");
      valider();
      break;

    default:
      // Caractère inconnu : ignoré
      break;
  }
}

// ===== Initialisation =====
void setup()
{
  Serial.begin(115200);  // Moniteur série (USB du ST-Link) pour le débogage
  BT.begin(38400);       // Vitesse du HC-05 : doit correspondre à sa configuration

  Wire.begin();                   // Démarre l'I2C (non utilisé pour l'instant)
  tft.begin();                    // Initialise l'écran
  tft.setRotation(0);             // Orientation portrait
  tft.fillScreen(ILI9341_BLACK);  // Fond noir

  actualiserEchiquier();          // Affiche le plateau, les pièces et le curseur

  Serial.println("Initialisation terminee");
  Serial.println("Pret, en attente du gamepad...");
}

// ===== Boucle principale =====
void loop()
{
  // Y a-t-il au moins un caractère reçu du téléphone ?
  if (BT.available() > 0)
  {
    char c = BT.read();  // Lire un caractère

    // Affichage de debug : caractère et son code ASCII
    Serial.print("Recu : '");
    Serial.print(c);
    Serial.print("' (code ");
    Serial.print((int)c);
    Serial.println(")");

    Commande(c);  // Exécuter l'action correspondante
  }
}

Credits

BYSGR
2 projects • 1 follower

Comments