From 6ee40225c01fe4af0cafbdc20ebe230f95342f1f Mon Sep 17 00:00:00 2001 From: Pascal RIGAUD Date: Mon, 9 Feb 2026 22:30:38 +0100 Subject: [PATCH] =?UTF-8?q?WIP=20am=C3=A9lioratuib=20de=20la=20com=20avec?= =?UTF-8?q?=20lichess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lichessboard.cpp | 707 +++++++++++++++++++++++-------------------- src/lichessboard.h | 76 +++-- src/main.cpp | 344 +++++++++++++++------ 3 files changed, 668 insertions(+), 459 deletions(-) diff --git a/src/lichessboard.cpp b/src/lichessboard.cpp index 6a221ee..72d949a 100644 --- a/src/lichessboard.cpp +++ b/src/lichessboard.cpp @@ -1,83 +1,50 @@ -#include "lichessboard.h" +#include "LichessBoard.h" -LichessBoard::LichessBoard() +LichessBoard::LichessBoard(const char *token) + : apiToken(token), streaming(false), lastHeartbeat(0) { - streaming = false; - lastHeartbeat = 0; - onMoveReceived = nullptr; - onGameStateChanged = nullptr; + client.setInsecure(); } -void LichessBoard::setToken(String token) -{ - apiToken = token; -} - -void LichessBoard::setMoveCallback(MoveCallback callback) +void LichessBoard::setMoveCallback(std::function callback) { onMoveReceived = callback; } -void LichessBoard::setGameStateCallback(GameStateCallback callback) +void LichessBoard::setGameStateCallback(std::function callback) { onGameStateChanged = callback; } -bool LichessBoard::streamGame(String gameId) +bool LichessBoard::connectToGame(const char *gameId) { - if (streaming) - { - stop(); - } - - currentGameId = gameId; - client.setInsecure(); // Pour HTTPS sans certificat - + currentGameId = String(gameId); Serial.println("Connexion à Lichess..."); + if (!client.connect("lichess.org", 443)) { - Serial.println("Échec connexion"); + Serial.println("❌ Échec connexion SSL"); return false; } - // Requête HTTP pour le stream - String request = "GET /api/board/game/stream/" + gameId + " HTTP/1.1\r\n"; - request += "Host: lichess.org\r\n"; - if (apiToken.length() > 0) - { - request += "Authorization: Bearer " + apiToken + "\r\n"; - } - request += "Accept: application/x-ndjson\r\n"; - request += "Connection: keep-alive\r\n"; - request += "\r\n"; + String request = String("GET /api/board/game/stream/") + gameId + " HTTP/1.1\r\n" + + "Host: lichess.org\r\n" + + "Authorization: Bearer " + apiToken + "\r\n" + + "Connection: keep-alive\r\n\r\n"; client.print(request); - Serial.println("Requête envoyée, attente réponse..."); - // Attendre les headers HTTP - unsigned long timeout = millis(); - while (client.connected() && !client.available()) - { - if (millis() - timeout > 5000) - { - Serial.println("Timeout headers"); - client.stop(); - return false; - } - delay(10); - } - - // Lire les headers - bool headersEnded = false; - while (client.available() && !headersEnded) + // Lire les headers HTTP + while (client.connected()) { String line = client.readStringUntil('\n'); + Serial.println("Header: " + line); + if (line == "\r" || line.length() == 0) { - headersEnded = true; + break; } - Serial.println("Header: " + line); } streaming = true; @@ -87,227 +54,6 @@ bool LichessBoard::streamGame(String gameId) return true; } -bool LichessBoard::streamMyGames() -{ - if (apiToken.length() == 0) - { - Serial.println("Token requis pour streamMyGames"); - return false; - } - - if (streaming) - { - stop(); - } - - client.setInsecure(); - - if (!client.connect("lichess.org", 443)) - { - Serial.println("Échec connexion"); - return false; - } - - String request = "GET /api/board/game/stream HTTP/1.1\r\n"; - request += "Host: lichess.org\r\n"; - request += "Authorization: Bearer " + apiToken + "\r\n"; - request += "Accept: application/x-ndjson\r\n"; - request += "Connection: keep-alive\r\n"; - request += "\r\n"; - - client.print(request); - - // Skip headers - while (client.connected()) - { - String line = client.readStringUntil('\n'); - if (line == "\r" || line.length() == 0) - break; - } - - streaming = true; - lastHeartbeat = millis(); - - return true; -} - -bool LichessBoard::streamEvents() -{ - if (apiToken.length() == 0) - { - Serial.println("Token requis"); - return false; - } - - if (streaming) - { - stop(); - } - - client.setInsecure(); - - if (!client.connect("lichess.org", 443)) - { - Serial.println("Échec connexion"); - return false; - } - - String request = "GET /api/stream/event HTTP/1.1\r\n"; - request += "Host: lichess.org\r\n"; - request += "Authorization: Bearer " + apiToken + "\r\n"; - request += "Accept: application/x-ndjson\r\n"; - request += "Connection: keep-alive\r\n"; - request += "\r\n"; - - client.print(request); - - // Skip headers - while (client.connected()) - { - String line = client.readStringUntil('\n'); - if (line == "\r" || line.length() == 0) - break; - } - - streaming = true; - lastHeartbeat = millis(); - - return true; -} - -void LichessBoard::parseLine(String line) -{ - line.trim(); - - if (line.length() == 0) - { - // Heartbeat (ligne vide) - lastHeartbeat = millis(); - return; - } - - Serial.println("JSON reçu: " + line); - - StaticJsonDocument<256> doc; - DeserializationError error = deserializeJson(doc, line); - - if (error) - { - Serial.print("Erreur JSON: "); - Serial.println(error.c_str()); - return; - } - - // Type d'événement - const char *type = doc["type"]; - - if (strcmp(type, "gameFull") == 0) - { - // État complet de la partie au début - Serial.println("=== Partie complète reçue ==="); - - const char *gameId = doc["id"]; - const char *state = doc["state"]["status"]; - - Serial.print("Game ID: "); - Serial.println(gameId); - Serial.print("État: "); - Serial.println(state); - - if (onGameStateChanged) - { - onGameStateChanged(String(state)); - } - - // Récupérer tous les coups déjà joués - const char *moves = doc["state"]["moves"]; - if (moves && strlen(moves) > 0) - { - Serial.print("Coups joués: "); - Serial.println(moves); - } - - // FEN initial si disponible - const char *initialFen = doc["initialFen"]; - if (initialFen) - { - Serial.print("FEN: "); - Serial.println(initialFen); - } - } - else if (strcmp(type, "gameState") == 0) - { - // Mise à jour de l'état (nouveau coup) - Serial.println("=== Nouveau coup ==="); - - const char *moves = doc["moves"]; - const char *status = doc["status"]; - - Serial.print("Tous les coups: "); - Serial.println(moves); - Serial.print("État: "); - Serial.println(status); - - if (onGameStateChanged) - { - onGameStateChanged(String(status)); - } - - // Extraire le dernier coup - if (moves && strlen(moves) > 0) - { - String allMoves = String(moves); - int lastSpace = allMoves.lastIndexOf(' '); - String lastMove = (lastSpace >= 0) ? allMoves.substring(lastSpace + 1) : allMoves; - - // Format UCI: e2e4 - if (lastMove.length() >= 4) - { - String from = lastMove.substring(0, 2); - String to = lastMove.substring(2, 4); - - Serial.print("Dernier coup: "); - Serial.print(from); - Serial.print(" -> "); - Serial.println(to); - - if (onMoveReceived) - { - onMoveReceived(from, to, lastMove, ""); - } - } - } - } - else if (strcmp(type, "gameStart") == 0) - { - // Nouvelle partie démarrée - JsonObject game = doc["game"]; - const char *gameId = game["id"]; - - Serial.print("Nouvelle partie: "); - Serial.println(gameId); - - if (onGameStateChanged) - { - onGameStateChanged("started"); - } - } - else if (strcmp(type, "gameFinish") == 0) - { - // Partie terminée - JsonObject game = doc["game"]; - const char *gameId = game["id"]; - - Serial.print("Partie terminée: "); - Serial.println(gameId); - - if (onGameStateChanged) - { - onGameStateChanged("finished"); - } - } -} - void LichessBoard::loop() { if (!streaming) @@ -315,99 +61,392 @@ void LichessBoard::loop() return; } - // Vérifier timeout if (millis() - lastHeartbeat > heartbeatTimeout) { - Serial.println("Timeout stream!"); + Serial.println("⏱️ Timeout stream!"); stop(); return; } - // Lire les données disponibles while (client.available()) { String line = client.readStringUntil('\n'); - parseLine(line); + line.trim(); + + // ✅ Ligne vide = heartbeat + if (line.length() == 0) + { + lastHeartbeat = millis(); + Serial.println("💓 Heartbeat"); + continue; + } + + // ✅ Ignorer chunks HTTP + if (isHexString(line)) + { + Serial.println("🔸 Chunk HTTP ignoré: " + line); + continue; + } + + // ✅ Traiter JSON + if (line[0] == '{') + { + parseLine(line); + } + else + { + Serial.println("⚠️ Ligne ignorée: " + line); + } } - // Vérifier connexion if (!client.connected()) { - Serial.println("Connexion perdue"); + Serial.println("❌ Connexion perdue"); stop(); } } void LichessBoard::stop() { - if (client.connected()) + if (streaming) { client.stop(); + streaming = false; + currentGameId = ""; + Serial.println("⏹️ Stream arrêté"); } - streaming = false; - currentGameId = ""; - Serial.println("Stream arrêté"); } -bool LichessBoard::isStreaming() +LichessBoard::Move LichessBoard::parseMove(const String &uci) { - return streaming; -} + Move move; + move.uci = uci; + move.from = uci.substring(0, 2); + move.to = uci.substring(2, 4); + move.isCastling = false; + move.isKingSide = false; + move.display = uci; // Par défaut, afficher l'UCI -String LichessBoard::getCurrentGameId() -{ - return currentGameId; -} - -bool LichessBoard::getGameState(String gameId, String &fen, String &lastMove) -{ - WiFiClientSecure httpClient; - httpClient.setInsecure(); - - if (!httpClient.connect("lichess.org", 443)) + // Détecter les roques + if ((move.from == "e1" || move.from == "e8") && + (move.to == "g1" || move.to == "g8" || + move.to == "c1" || move.to == "c8")) { - return false; - } - String request = "GET /game/export/" + gameId + "?moves=true&clocks=false HTTP/1.1\r\n"; - request += "Host: lichess.org\r\n"; - request += "Accept: application/json\r\n"; - request += "Connection: close\r\n"; - request += "\r\n"; + move.isCastling = true; - httpClient.print(request); - - // Skip headers - while (httpClient.connected()) - { - String line = httpClient.readStringUntil('\n'); - if (line == "\r") - break; - } - - // Lire JSON - String json = httpClient.readString(); - httpClient.stop(); - - StaticJsonDocument<4096> doc; - DeserializationError error = deserializeJson(doc, json); - - if (error) - { - return false; - } - - const char *moves = doc["moves"]; - if (moves) - { - lastMove = String(moves); - // Extraire le dernier coup - int lastSpace = lastMove.lastIndexOf(' '); - if (lastSpace >= 0) + if (move.to[0] == 'g') { - lastMove = lastMove.substring(lastSpace + 1); + // Petit roque + move.isKingSide = true; + move.display = "O-O"; + Serial.println("🏰 PETIT ROQUE (O-O)"); + } + else + { + // Grand roque + move.isKingSide = false; + move.display = "O-O-O"; + Serial.println("🏰 GRAND ROQUE (O-O-O)"); } } + return move; +} + +void LichessBoard::parseLine(const String &line) +{ + lastHeartbeat = millis(); + + StaticJsonDocument<2048> doc; + DeserializationError error = deserializeJson(doc, line); + + if (error) + { + Serial.print("❌ Erreur JSON: "); + Serial.println(error.c_str()); + return; + } + + Serial.print("📦 JSON reçu ("); + Serial.print(line.length()); + Serial.println(" bytes)"); + + if (!doc.containsKey("type")) + { + Serial.println("⚠️ Pas de champ 'type'"); + return; + } + + const char *type = doc["type"]; + Serial.print("✅ Type: "); + Serial.println(type); + + if (strcmp(type, "gameFull") == 0) + { + Serial.println("=== Partie complète reçue ==="); + + if (doc.containsKey("id")) + { + const char *gameId = doc["id"]; + if (gameId) + { + Serial.print("Game ID: "); + Serial.println(gameId); + } + } + + if (doc["state"].containsKey("status")) + { + const char *state = doc["state"]["status"]; + if (state) + { + Serial.print("État: "); + Serial.println(state); + Serial.println(); + + if (onGameStateChanged) + { + onGameStateChanged(String(state)); + } + } + } + + Serial.println("📢 État partie: " + String(doc["state"]["status"].as())); + + if (doc["state"].containsKey("moves")) + { + String moves = doc["state"]["moves"].as(); + Serial.print("Coups joués: "); + Serial.println(moves); + } + + if (doc.containsKey("initialFen")) + { + Serial.print("FEN: "); + Serial.println(doc["initialFen"].as()); + } + else + { + Serial.println("FEN: startpos"); + } + } + else if (strcmp(type, "gameState") == 0) + { + Serial.println("=== Nouveau coup ==="); + + if (doc.containsKey("moves")) + { + String allMoves = doc["moves"].as(); + Serial.print("Tous les coups: "); + Serial.println(allMoves); + + if (allMoves.length() > 0) + { + int lastSpace = allMoves.lastIndexOf(' '); + String lastMoveUci = (lastSpace >= 0) ? allMoves.substring(lastSpace + 1) : allMoves; + + if (lastMoveUci.length() >= 4) + { + // ✅ Parser le coup + lastMove = parseMove(lastMoveUci); + + // Affichage avec notation standard + Serial.print("🎯 Coup: "); + Serial.println(lastMove.display); + + if (lastMove.isCastling) + { + Serial.print(" Détails: "); + Serial.print(lastMove.from); + Serial.print(" -> "); + Serial.print(lastMove.to); + Serial.print(" ("); + Serial.print(lastMove.isKingSide ? "petit roque" : "grand roque"); + Serial.println(")"); + } + + Serial.println("\n🎯 NOUVEAU COUP:"); + Serial.println(" De: " + lastMove.from); + Serial.println(" À: " + lastMove.to); + Serial.println(" Notation: " + lastMove.display); + + if (doc.containsKey("status")) + { + Serial.print("État: "); + Serial.println(doc["status"].as()); + Serial.println(); + } + + if (onMoveReceived) + { + String fen = doc.containsKey("fen") ? doc["fen"].as() : ""; + onMoveReceived(lastMove.from, lastMove.to, lastMove.uci, fen); + } + } + } + } + + if (doc.containsKey("status")) + { + const char *status = doc["status"]; + Serial.println("📢 État partie: " + String(status)); + + if (onGameStateChanged) + { + onGameStateChanged(String(status)); + } + } + } + else if (strcmp(type, "chatLine") == 0) + { + Serial.println("💬 Message chat reçu"); + } + else + { + Serial.println("⚠️ Type inconnu: " + String(type)); + } +} + +bool LichessBoard::isHexString(const String &str) +{ + if (str.length() == 0 || str.length() > 8) + return false; + + for (char c : str) + { + if (!isxdigit(c)) + return false; + } + return true; +} + +bool LichessBoard::streamCurrentGame() +{ + Serial.println("🔍 Recherche de la partie en cours..."); + + WiFiClientSecure tempClient; + tempClient.setInsecure(); + + if (!tempClient.connect("lichess.org", 443)) + { + Serial.println("❌ Échec connexion pour récupérer les parties"); + return false; + } + + // Requête pour obtenir les parties en cours + String request = "GET /api/account/playing HTTP/1.1\r\n" + + String("Host: lichess.org\r\n") + + "Authorization: Bearer " + apiToken + "\r\n" + + "Accept: application/json\r\n" + + "Connection: close\r\n\r\n"; + + tempClient.print(request); + + // Lire les headers + while (tempClient.connected()) + { + String line = tempClient.readStringUntil('\n'); + if (line == "\r" || line.length() == 0) + { + break; + } + } + + // Lire le body JSON + String jsonResponse = ""; + while (tempClient.available()) + { + jsonResponse += tempClient.readString(); + } + tempClient.stop(); + + Serial.println("📥 Réponse API:"); + Serial.println(jsonResponse); + + // ⚠️ ALLOCATION DYNAMIQUE SUR LE HEAP + DynamicJsonDocument *doc = new DynamicJsonDocument(4096); + DeserializationError error = deserializeJson(*doc, jsonResponse); + + if (error) + { + Serial.print("❌ Erreur parsing JSON: "); + Serial.println(error.c_str()); + return false; + } + + // Vérifier s'il y a des parties en cours + if (!(*doc).containsKey("nowPlaying") || (*doc)["nowPlaying"].size() == 0) + { + Serial.println("❌ Aucune partie en cours"); + return false; + } + + // Récupérer la première partie (la plus récente) + JsonObject firstGame = (*doc)["nowPlaying"][0]; + const char *gameId = firstGame["gameId"]; + + if (!gameId) + { + Serial.println("❌ Impossible de récupérer l'ID de partie"); + return false; + } + + Serial.print("✅ Partie trouvée: "); + Serial.println(gameId); + + // Afficher infos supplémentaires + if (firstGame.containsKey("opponent")) + { + const char *opponent = firstGame["opponent"]["username"]; + Serial.print(" Adversaire: "); + Serial.println(opponent); + } + + if (firstGame.containsKey("isMyTurn")) + { + bool myTurn = firstGame["isMyTurn"]; + Serial.print(" Mon tour: "); + Serial.println(myTurn ? "Oui" : "Non"); + } + + // Se connecter au stream de cette partie + return connectToGame(gameId); +} + +bool LichessBoard::streamMyGames() +{ + Serial.println("🎮 Démarrage du stream de toutes mes parties..."); + + if (!client.connect("lichess.org", 443)) + { + Serial.println("❌ Échec connexion SSL"); + return false; + } + + String request = "GET /api/board/game/stream/incoming HTTP/1.1\r\n" + + String("Host: lichess.org\r\n") + + "Authorization: Bearer " + apiToken + "\r\n" + + "Connection: keep-alive\r\n\r\n"; + + client.print(request); + Serial.println("📡 Stream de toutes les parties démarré"); + + // Lire les headers + while (client.connected()) + { + String line = client.readStringUntil('\n'); + Serial.println("Header: " + line); + + if (line == "\r" || line.length() == 0) + { + break; + } + } + + streaming = true; + lastHeartbeat = millis(); + Serial.println("✅ Stream actif!"); + return true; } diff --git a/src/lichessboard.h b/src/lichessboard.h index 04aca9a..3e5e0e8 100644 --- a/src/lichessboard.h +++ b/src/lichessboard.h @@ -5,55 +5,51 @@ #include #include -// Callback pour chaque coup reçu -typedef void (*MoveCallback)(String from, String to, String san, String fen); -typedef void (*GameStateCallback)(String state); // started, finished, etc. - class LichessBoard { +public: + struct Move + { + String from; + String to; + String uci; // Notation UCI (ex: e1g1) + String display; // Notation affichage (ex: O-O) + bool isCastling; + bool isKingSide; + }; + + LichessBoard(const char *token); + + void setMoveCallback(std::function callback); + void setGameStateCallback(std::function callback); + + bool connectToGame(const char *gameId); + void loop(); + void stop(); + + bool isStreaming() const { return streaming; } + String getCurrentGameId() const { return currentGameId; } + Move getLastMove() const { return lastMove; } + + bool streamCurrentGame(); // Stream la partie en cours + bool streamMyGames(); // Stream toutes les parties en cours + private: WiFiClientSecure client; - String apiToken; - String currentGameId; + const char *apiToken; bool streaming; - String lastLine; - - MoveCallback onMoveReceived; - GameStateCallback onGameStateChanged; + String currentGameId; + Move lastMove; unsigned long lastHeartbeat; - const unsigned long heartbeatTimeout = 10000; // 10 secondes + const unsigned long heartbeatTimeout = 35000; - // Parse une ligne JSON du stream - void parseLine(String line); + std::function onMoveReceived; + std::function onGameStateChanged; -public: - LichessBoard(); - - // Configuration - void setToken(String token); - void setMoveCallback(MoveCallback callback); - void setGameStateCallback(GameStateCallback callback); - - // Stream d'une partie spécifique - bool streamGame(String gameId); - - // Stream de vos parties en cours (nécessite token) - bool streamMyGames(); - - // Stream des événements de votre compte - bool streamEvents(); - - // À appeler dans loop() - void loop(); - - // Contrôle - void stop(); - bool isStreaming(); - String getCurrentGameId(); - - // Récupérer l'état actuel d'une partie (non-stream) - bool getGameState(String gameId, String &fen, String &lastMove); + void parseLine(const String &line); + bool isHexString(const String &str); + Move parseMove(const String &uci); }; #endif diff --git a/src/main.cpp b/src/main.cpp index d7e3ac0..ccd39f0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,121 +1,295 @@ #include -#include -#include +#include #include #include "LichessBoard.h" +const char *ssid = "home"; +const char *password = "0123456789"; +const char *lichessToken = "lip_ccje7pt8qzmrVT48d1V8"; + WebServer server(80); -WiFiManager wm; -LichessBoard lichess; +LichessBoard lichess(lichessToken); +String lastMoveDisplay = ""; String currentGameId = ""; +String gameStatus = "Aucune partie"; -// Callback quand un coup est joué -void onMove(String from, String to, String san, String fen) -{ - Serial.println("\n🎯 NOUVEAU COUP:"); - Serial.println(" De: " + from); - Serial.println(" À: " + to); - Serial.println(" Notation: " + san); +// ========== PAGE WEB ========== +const char *htmlPage = R"rawliteral( + + + + + + Lichess Board Stream + + + +
+

♟️ Lichess Board Stream

+ +
+
⏸️ En attente
+
+ +
--
+ +
+ Partie: Aucune
+ État: -- +
+ + + +
+ OU +
+ + + + + +
- // ICI: Allumer des LEDs, bouger des servos, etc. -} - -// Callback changement d'état -void onGameState(String state) -{ - Serial.println("\n📢 État partie: " + state); -} + + + +)rawliteral"; void setup() { Serial.begin(115200); + Serial.println("\n\n=== DÉMARRAGE ==="); - // WiFiManager - Portail captif automatique - // Si pas de WiFi configuré, crée un AP "ESP32-Setup" - wm.setConfigPortalTimeout(180); // 3 min timeout - - if (!wm.autoConnect("ESP32-Setup", "12345678")) + // Connexion WiFi + WiFi.begin(ssid, password); + Serial.print("Connexion WiFi"); + while (WiFi.status() != WL_CONNECTED) { - Serial.println("Échec connexion WiFi"); - ESP.restart(); + delay(500); + Serial.print("."); } - - Serial.println("WiFi connecté!"); + Serial.println("\n✅ WiFi connecté!"); Serial.print("IP: "); Serial.println(WiFi.localIP()); - // Configuration Lichess - lichess.setMoveCallback(onMove); - lichess.setGameStateCallback(onGameState); - - // OPTIONNEL: Token API pour vos parties privées - // Générer sur: https://lichess.org/account/oauth/token - lichess.setToken("lip_ccje7pt8qzmrVT48d1V8"); - - // Interface web - server.on("/", []() - { - String html = ""; - html += "

🎮 ESP32 Lichess Board

"; + // ========== CALLBACKS LICHESS ========== + lichess.setMoveCallback([](String from, String to, String uci, String fen) + { + LichessBoard::Move move = lichess.getLastMove(); + lastMoveDisplay = move.display; - if (lichess.isStreaming()) { - html += "
✅ Stream actif: " + lichess.getCurrentGameId() + "
"; - html += ""; - } else { - html += "
⭕ Aucun stream actif
"; - html += "
"; - html += ""; - html += ""; - html += "
"; - } + Serial.println("\n━━━━━━━━━━━━━━━━━━━━━━━━"); + Serial.println("🔔 NOUVEAU COUP REÇU!"); + Serial.println("━━━━━━━━━━━━━━━━━━━━━━━━"); + Serial.println(" Notation: " + move.display); - html += "

Exemples d'ID:

"; - html += "
  • Partie TV Lichess: https://lichess.org/abc12345
"; - html += "🔄 OTA Update"; - html += ""; - - server.send(200, "text/html", html); }); - - server.on("/stream", HTTP_POST, []() - { - if (server.hasArg("gameid")) { - currentGameId = server.arg("gameid"); - currentGameId.trim(); - - if (lichess.streamGame(currentGameId)) { - server.sendHeader("Location", "/"); - server.send(303); + if (move.isCastling) { + Serial.println(" ⚠️ C'est un ROQUE!"); + if (move.isKingSide) { + Serial.println(" Type: Petit roque (O-O)"); + if (from == "e1") { + Serial.println(" Blancs: ♔ e1→g1, ♖ h1→f1"); + } else { + Serial.println(" Noirs: ♚ e8→g8, ♜ h8→f8"); + } } else { - server.send(500, "text/plain", "Erreur démarrage stream"); + Serial.println(" Type: Grand roque (O-O-O)"); + if (from == "e1") { + Serial.println(" Blancs: ♔ e1→c1, ♖ a1→d1"); + } else { + Serial.println(" Noirs: ♚ e8→c8, ♜ a8→d8"); + } } } else { - server.send(400, "text/plain", "ID manquant"); + Serial.println(" Type: Coup normal"); + Serial.println(" De: " + from + " → À: " + to); + } + Serial.println("━━━━━━━━━━━━━━━━━━━━━━━━\n"); }); + + lichess.setGameStateCallback([](String state) + { + gameStatus = state; + Serial.println("🎮 État partie: " + state); }); + + // ========== ROUTES WEB ========== + + // Page principale + server.on("/", HTTP_GET, []() + { server.send(200, "text/html", htmlPage); }); + + // API JSON + server.on("/json", HTTP_GET, []() + { + StaticJsonDocument<512> doc; + + doc["streaming"] = lichess.isStreaming(); + doc["gameId"] = lichess.getCurrentGameId(); + doc["lastMove"] = lastMoveDisplay; + doc["status"] = gameStatus; + doc["connected"] = WiFi.status() == WL_CONNECTED; + doc["uptime"] = millis() / 1000; + + String json; + serializeJson(doc, json); + + server.send(200, "application/json", json); }); + + // ⭐ NOUVELLE ROUTE: Stream partie en cours + server.on("/stream-current", HTTP_GET, []() + { + if (lichess.streamCurrentGame()) { + server.send(200, "text/plain", "✅ Stream démarré sur votre partie en cours"); + } else { + server.send(500, "text/plain", "❌ Aucune partie en cours ou erreur"); } }); - server.on("/stop", []() + // Stream partie spécifique + server.on("/start", HTTP_GET, []() + { + if (server.hasArg("gameId")) { + currentGameId = server.arg("gameId"); + if (lichess.connectToGame(currentGameId.c_str())) { + server.send(200, "text/plain", "✅ Stream démarré: " + currentGameId); + } else { + server.send(500, "text/plain", "❌ Erreur connexion"); + } + } else { + server.send(400, "text/plain", "❌ Paramètre gameId manquant"); + } }); + + // Arrêter le stream + server.on("/stop", HTTP_GET, []() { lichess.stop(); - server.sendHeader("Location", "/"); - server.send(303); }); + server.send(200, "text/plain", "⏹️ Stream arrêté"); }); - // ElegantOTA - Interface web élégante pour OTA - ElegantOTA.begin(&server); server.begin(); - Serial.println("HTTP server + OTA démarrés"); - Serial.println("Ouvrez: http://" + WiFi.localIP().toString()); - lichess.streamMyGames(); + Serial.println("🌐 Serveur web démarré"); + Serial.println("👉 Ouvrez http://" + WiFi.localIP().toString()); } void loop() { server.handleClient(); - ElegantOTA.loop(); - lichess.loop(); // ⚠️ IMPORTANT: appeler dans loop() + lichess.loop(); }