#include "LichessBoard.h" LichessBoard::LichessBoard(const char *token) : apiToken(token), streaming(false), lastHeartbeat(0) { client.setInsecure(); } void LichessBoard::setMoveCallback(std::function callback) { onMoveReceived = callback; } void LichessBoard::setGameStateCallback(std::function callback) { onGameStateChanged = callback; } bool LichessBoard::connectToGame(const char *gameId) { currentGameId = String(gameId); Serial.println("Connexion à Lichess..."); if (!client.connect("lichess.org", 443)) { Serial.println("❌ Échec connexion SSL"); return false; } 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..."); // Lire les headers HTTP 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 démarré!"); return true; } void LichessBoard::loop() { if (!streaming) { return; } if (millis() - lastHeartbeat > heartbeatTimeout) { Serial.println("⏱️ Timeout stream!"); stop(); return; } while (client.available()) { String line = client.readStringUntil('\n'); 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); } } if (!client.connected()) { Serial.println("❌ Connexion perdue"); stop(); } } void LichessBoard::stop() { if (streaming) { client.stop(); streaming = false; currentGameId = ""; Serial.println("⏹️ Stream arrêté"); } } LichessBoard::Move LichessBoard::parseMove(const String &uci) { 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 // Détecter les roques if ((move.from == "e1" || move.from == "e8") && (move.to == "g1" || move.to == "g8" || move.to == "c1" || move.to == "c8")) { move.isCastling = true; if (move.to[0] == 'g') { // 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; }