diff --git a/arduino/controller/controller.ino b/arduino/controller/controller.ino index 022030758d9b1cd0b7b464a96db9958833f6d0ed..63bf078aea6cba4709cbe44c2d5261504f9b3ba9 100644 --- a/arduino/controller/controller.ino +++ b/arduino/controller/controller.ino @@ -1,100 +1,146 @@ #include "SevSeg.h" -// Bibliothèque pour gérer l'afficheur 7 segments sans gérer chaque LED manuellement +// ======================================================================================= +// VARIABLES GLOBALES +// ======================================================================================= + +// Bibliothèque pour gérer l'afficheur 7 segments SevSeg sevseg; -// --- VARIABLES DE JEU --- -// Ces variables stockent les données reçues du serveur Node.js +// --- VARIABLES DE JEU (Reçues du PC) --- int globalScore = 0; int globalDeaths = 0; -// --- VARIABLES BOUTON (INPUT) --- -const int buttonPin = A0; // Le bouton est branché sur A0 -int buttonState = HIGH; // État actuel (HIGH par défaut car on utilise INPUT_PULLUP) -int lastButtonState = HIGH; // État précédent (pour détecter les changements) -// Variables pour l'anti-rebond (Debounce) -// Sert à éviter qu'un seul appui soit compté comme 10 clics à cause des vibrations mécaniques +// ======================================================================================= +// ------- BOUTON ------- +// ======================================================================================= +const int buttonPin = A0; +int buttonState = HIGH; +int lastButtonState = HIGH; unsigned long lastDebounceTime = 0; -unsigned long debounceDelay = 50; // On attend 50ms pour valider un appui stable +unsigned long debounceDelay = 50; + + +// ======================================================================================= +// ------- CODEUR ROTATIF ------- +// ======================================================================================= +#define CLK A1 +#define DT A2 +int encoderValue = 6; // Vitesse par défaut +int currentStateCLK; +int lastStateCLK; + + +// ======================================================================================= +// ------- SETUP ------- +// ======================================================================================= void setup() { - // Communication Série - // Vitesse réglée à 9600 bauds pour être synchro avec le serveur Node.js + // Communication Série (Vitesse 9600 pour stabilité) Serial.begin(9600); - // Timeout très court (2ms) pour éviter que l'Arduino bloque en attendant des données - // Cela corrige le bug de luminosité inégale sur l'afficheur - Serial.setTimeout(2); + Serial.setTimeout(2); // Timeout court pour ne pas bloquer l'affichage - // Configuration du Bouton - // INPUT_PULLUP active la résistance interne de l'Arduino. - // Conséquence : Le bouton est HIGH au repos, et passe LOW quand on appuie (connecté au GND). + // ------------------ CONFIG BOUTON ------------------ pinMode(buttonPin, INPUT_PULLUP); - // Configuration de l'Afficheur 4 Digits + // ------------------ CONFIG CODEUR ROTATIF ------------------ + pinMode(CLK, INPUT); + pinMode(DT, INPUT); + // Lecture de l'état initial du codeur + lastStateCLK = digitalRead(CLK); + + // ------------------ CONFIG 4 DIGIT 7 SEGMENT ------------------ byte numDigits = 4; - byte digitPins[] = {10, 11, 12, 13}; // Pins pour choisir quel chiffre s'allume - byte segmentPins[] = {2, 3, 4, 5, 6, 7, 8, 9}; // Pins pour dessiner les segments (A-G + Point) + byte digitPins[] = {10, 11, 12, 13}; + byte segmentPins[] = {2, 3, 4, 5, 6, 7, 8, 9}; bool resistorsOnSegments = false; - byte hardwareConfig = COMMON_CATHODE; // Type d'écran (Cathode commune) - bool updateWithDelays = false; // False recommandé pour ne pas bloquer le code - bool leadingZeros = true; // Important : Affiche "05" au lieu de "5" pour garder l'alignement + byte hardwareConfig = COMMON_CATHODE; + bool updateWithDelays = false; + bool leadingZeros = true; bool disableDecPoint = false; - // Initialisation de l'objet écran sevseg.begin(hardwareConfig, numDigits, digitPins, segmentPins, resistorsOnSegments, updateWithDelays, leadingZeros, disableDecPoint); - sevseg.setBrightness(90); // Luminosité + sevseg.setBrightness(90); sevseg.setNumber(0); } + +// ======================================================================================= +// ------- BOUCLE ------- +// ======================================================================================= void loop() { - // ---RAFRAÎCHISSEMENT DE L'ÉCRAN --- - // Cette fonction doit être appelée en permanence pour créer l'illusion d'optique (persistance rétinienne) + + // --------------------------------------------------------- + // 1. REFRESH DISPLAY (Priorité Absolue) + // --------------------------------------------------------- sevseg.refreshDisplay(); - // ---LOGIQUE DU BOUTON (JUMP) --- + + // --------------------------------------------------------- + // 2. LOGIQUE BOUTON (Jump) + // --------------------------------------------------------- int reading = digitalRead(buttonPin); - // Si on détecte un changement (bruit ou vrai appui), on reset le chrono if (reading != lastButtonState) { lastDebounceTime = millis(); } - // Si l'état est stable depuis 50ms (le signal ne rebondit plus) if ((millis() - lastDebounceTime) > debounceDelay) { - - // Si l'état a changé par rapport à ce qu'on avait mémorisé if (reading != buttonState) { buttonState = reading; - - // C'EST ICI QU'ON ENVOIE LE JUMP AU PC - // On vérifie LOW car en INPUT_PULLUP, appuyer = connecter au GND (0V) if (buttonState == LOW) { - Serial.println("JUMP"); + Serial.println("JUMP"); // Envoi de l'action de saut } } } - - // On sauvegarde la lecture pour la prochaine boucle lastButtonState = reading; - // ---RÉCEPTION DES DONNÉES DU JEU (OUTPUT) --- - // On écoute si le PC nous envoie des mises à jour (Score ou Morts) + // --------------------------------------------------------- + // 3. LOGIQUE CODEUR ROTATIF (Vitesse) + // --------------------------------------------------------- + currentStateCLK = digitalRead(CLK); + + // Si l'état a changé et que c'est un front montant (passage à 1) + if (currentStateCLK != lastStateCLK && currentStateCLK == 1){ + + // Détermination du sens de rotation + if (digitalRead(DT) != currentStateCLK) { + // Sens Horaire : Accélérer + encoderValue--; + } else { + // Sens Anti-Horaire : Ralentir + encoderValue++; + } + + // Limites de sécurité (Vitesse min 1, max 20) + if (encoderValue < 1) encoderValue = 1; + if (encoderValue > 20) encoderValue = 20; + + // Envoi de la commande de vitesse au PC (Format "V6") + Serial.print("V"); + Serial.println(encoderValue); + } + // Mise à jour de l'état précédent du CLK + lastStateCLK = currentStateCLK; + + + // --------------------------------------------------------- + // 4. RECEPTION SERIAL (Score / Morts) + // --------------------------------------------------------- + // On lit tant qu'il y a des données pour vider le buffer while (Serial.available()) { char c = (char)Serial.read(); - // Protocole défini avec le back : 'S' suivi du score (ex: S12) if (c == 'S') { int val = Serial.parseInt(); - // On limite à 99 car on n'a que 2 chiffres dédiés au score sur l'écran if (val > 99) val = 99; globalScore = val; } - // Protocole : 'D' suivi des morts (ex: D05) else if (c == 'D') { int val = Serial.parseInt(); if (val > 99) val = 99; @@ -102,12 +148,11 @@ void loop() { } } - // --- D. CALCUL ET AFFICHAGE FINAL --- - // Astuce mathématique pour afficher 2 infos sur un seul écran : - // Les milliers/centaines sont le Score, les dizaines/unités sont les Morts. - // Score x, Morts y => x.y - int displayValue = (globalScore * 100) + globalDeaths; - // Le '2' indique qu'on allume le point décimal à la position 2 (SS.DD) pour séparer visuellement + // --------------------------------------------------------- + // 5. UPDATE DISPLAY + // --------------------------------------------------------- + // Score à gauche, Morts à droite (Format 00.00) + int displayValue = (globalScore * 100) + globalDeaths; sevseg.setNumber(displayValue, 2); } \ No newline at end of file diff --git a/game/client/src/Game.tsx b/game/client/src/Game.tsx index 7639057ceb2701de0fce39e9eceb531e67bb83f1..7e3476d5325d7991e2552fdde9556748292474a4 100644 --- a/game/client/src/Game.tsx +++ b/game/client/src/Game.tsx @@ -24,10 +24,12 @@ export default function Game() { const wsRef = useRef(null); const animationFrameRef = useRef(null); - const [score, setScore] = useState(0); const [deathCount, setDeathCount] = useState(0); const [alive, setAlive] = useState(true); + + // AJOUT: Pour afficher la vitesse à l'écran + const [displaySpeed, setDisplaySpeed] = useState(6); const player = useRef({ x: 80, @@ -42,7 +44,10 @@ export default function Game() { const groundHeight = 80; const obstacles = useRef([]); - const gameSpeed = 6; + + // MODIFICATION: On utilise une Ref pour la vitesse du jeu (Physique) + // Cela permet de changer la valeur sans casser la boucle d'animation + const gameSpeedRef = useRef(6); // ------- JUMP ------- function jump() { @@ -60,7 +65,18 @@ export default function Game() { ws.onmessage = (msg) => { try { const data = JSON.parse(msg.data); + + // Gestion du SAUT if (String(data.action).toLowerCase() === "jump") jump(); + + // AJOUT: Gestion de la VITESSE + if (data.type === "speed") { + // On met à jour la physique immédiatement + gameSpeedRef.current = data.value; + // On met à jour l'affichage + setDisplaySpeed(data.value); + } + } catch (e) { console.error("Invalid WS message", e); } @@ -142,9 +158,12 @@ export default function Game() { } // Move obstacles and scoring + // MODIFICATION: On utilise la vitesse dynamique (Ref) + const currentSpeed = gameSpeedRef.current; + obstacles.current = obstacles.current .map((o) => { - const newO = { ...o, x: o.x - gameSpeed }; + const newO = { ...o, x: o.x - currentSpeed }; if (!newO.counted && newO.x + newO.width < P.x) { newO.counted = true; @@ -190,7 +209,7 @@ export default function Game() { }, [alive]); -// ------- RESTART ------- + // ------- RESTART ------- function restart() { if (animationFrameRef.current !== null) { cancelAnimationFrame(animationFrameRef.current); @@ -201,11 +220,8 @@ export default function Game() { player.current.grounded = false; obstacles.current = []; - - setScore(0); - - sendToServer({ type: "score", score: 0 }); - + setScore(0); + sendToServer({ type: "score", score: 0 }); // Important pour l'arduino setAlive(true); } @@ -236,19 +252,34 @@ export default function Game() { style={{ width: "100%", height: "100%" }} /> -
- Score: {score}
- Deaths: {deathCount} + +
Score: {score}
+
+
Deaths: {deathCount}
+ {/* AJOUT: Affichage de la vitesse */} +
Speed: {displaySpeed}
+
{!alive && ( -
-

Game Over

-
)} ); -} +} \ No newline at end of file diff --git a/game/server/db/db.js b/game/server/db/db.js index 886063fb0c3504d65ef78c1addd207a5c846facf..74b39a5f7b11bc699124f777d86c28f035a9acc0 100644 --- a/game/server/db/db.js +++ b/game/server/db/db.js @@ -15,40 +15,63 @@ const pool = new Pool({ host: process.env.DB_HOST || 'localhost', }); -// --- Configuration du Buffer d'Ecriture --- -// Stockage temporaire des parties terminees avant insertion en base -let writeBuffer = []; +// --- Configuration des Buffers d'Ecriture --- +// Stockage temporaire des parties terminees +let runBuffer = []; +// Stockage temporaire des événements de jeu +let eventBuffer = []; + // Intervalle de temps entre chaque ecriture en base (10 secondes) const FLUSH_INTERVAL = 10000; // --- Initialisation de la Base de Donnees --- async function initDB() { try { - // Creation de la table 'game_runs' si elle n'existe pas - // Inclut l'identifiant du capteur pour la tracabilite des donnees - await pool.query(` - CREATE TABLE IF NOT EXISTS game_runs ( - id SERIAL PRIMARY KEY, - sensor_id VARCHAR(50) NOT NULL, - final_score INTEGER NOT NULL, - total_deaths INTEGER NOT NULL, - played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - `); - console.log("Connexion BDD etablie et schema verifie."); + const client = await pool.connect(); + try { + await client.query('BEGIN'); + + // 1. Creation de la table 'game_runs' + await client.query(` + CREATE TABLE IF NOT EXISTS game_runs ( + id SERIAL PRIMARY KEY, + sensor_id VARCHAR(50) NOT NULL, + final_score INTEGER NOT NULL, + total_deaths INTEGER NOT NULL, + played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + // 2. Creation de la table 'game_events' + // Stocke le JSON de l'événement sous forme de texte + await client.query(` + CREATE TABLE IF NOT EXISTS game_events ( + id SERIAL PRIMARY KEY, + sensor_id VARCHAR(50) NOT NULL, + event_data TEXT NOT NULL, + occurred_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + await client.query('COMMIT'); + console.log("Connexion BDD etablie et schemas verifies."); + } catch (err) { + await client.query('ROLLBACK'); + throw err; + } finally { + client.release(); + } } catch (err) { console.error("Erreur critique BDD :", err.message); } } -// --- Fonction d'Ajout au Buffer --- -// Appelee par le serveur lorsqu'une partie se termine (Signal Restart) +// --- Fonction d'Ajout au Buffer (Parties) --- function bufferGameRun(sensorId, score, deaths) { if (score > 0) { - console.log(`Ajout au buffer de persistence : Capteur [${sensorId}] - Score ${score}`); - - // Ajout de l'entree dans la memoire tampon avec un timestamp generé par le serveur - writeBuffer.push({ + console.log(`Ajout au buffer 'Runs' : Capteur [${sensorId}] - Score ${score}`); + + runBuffer.push({ sensorId: sensorId, score: score, deaths: deaths, @@ -57,30 +80,58 @@ function bufferGameRun(sensorId, score, deaths) { } } +// --- Fonction d'Ajout au Buffer (Evenements) --- +// Convertit l'objet event en string JSON avant le stockage +function bufferGameEvents(sensorId, eventData) { + // On s'assure que eventData est une chaine pour la colonne TEXT + const eventString = typeof eventData === 'object' ? JSON.stringify(eventData) : eventData; + + // console.log(`Ajout au buffer 'Events' : Capteur [${sensorId}]`); // Decommenter pour debug verbeux + + eventBuffer.push({ + sensorId: sensorId, + eventData: eventString, + date: new Date() + }); +} + // --- Fonction de Vidage du Buffer (Flush) --- // Executee periodiquement pour inserer les donnees en lot (Batch Insert) async function flushBuffer() { - // Si le buffer est vide, aucune action n'est requise - if (writeBuffer.length === 0) return; + // Si les deux buffers sont vides, on ne fait rien + if (runBuffer.length === 0 && eventBuffer.length === 0) return; - // Copie locale du buffer et reinitialisation immediate pour ne pas bloquer les nouvelles entrees - const currentBatch = [...writeBuffer]; - writeBuffer = []; + // Copie locale des buffers et reinitialisation immediate + const currentBatchRuns = [...runBuffer]; + const currentBatchEvents = [...eventBuffer]; + + runBuffer = []; + eventBuffer = []; - console.log(`Traitement du lot : sauvegarde de ${currentBatch.length} entrées en base...`); + const totalOps = currentBatchRuns.length + currentBatchEvents.length; + console.log(`Traitement du lot : sauvegarde de ${totalOps} entrées en base...`); const client = await pool.connect(); try { // Debut de la transaction SQL await client.query('BEGIN'); - for (const run of currentBatch) { + // 1. Insertion des Runs + for (const run of currentBatchRuns) { await client.query( 'INSERT INTO game_runs(sensor_id, final_score, total_deaths, played_at) VALUES($1, $2, $3, $4)', [run.sensorId, run.score, run.deaths, run.date] ); } + // 2. Insertion des Events + for (const evt of currentBatchEvents) { + await client.query( + 'INSERT INTO game_events(sensor_id, event_data, occurred_at) VALUES($1, $2, $3)', + [evt.sensorId, evt.eventData, evt.date] + ); + } + // Validation de la transaction await client.query('COMMIT'); console.log("Sauvegarde BDD reussie."); @@ -88,6 +139,9 @@ async function flushBuffer() { // Annulation en cas d'erreur pour garantir l'integrite des donnees await client.query('ROLLBACK'); console.error("Echec de la sauvegarde BDD :", e.message); + + // Optionnel : En cas d'échec critique, on pourrait remettre les items dans le buffer + // pour retenter plus tard, mais attention aux boucles infinies d'erreurs. } finally { // Liberation du client du pool client.release(); @@ -97,4 +151,4 @@ async function flushBuffer() { // Lancement du timer pour le vidage automatique du buffer setInterval(flushBuffer, FLUSH_INTERVAL); -module.exports = { initDB, bufferGameRun }; \ No newline at end of file +module.exports = { initDB, bufferGameRun, bufferGameEvents }; \ No newline at end of file diff --git a/game/server/server.js b/game/server/server.js index 771b2bf21a88223728f17a79026100c416e4ac76..0cee959d25648b7cf75a2fc07b898022ca558d66 100644 --- a/game/server/server.js +++ b/game/server/server.js @@ -9,9 +9,10 @@ const minimist = require('minimist'); const express = require('express'); const { SerialPort, ReadlineParser } = require('serialport'); const WebSocket = require('ws'); -const { initDB, bufferGameRun } = require('./db/db'); +// Import de la nouvelle fonction bufferGameEvents +const { initDB, bufferGameRun, bufferGameEvents } = require('./db/db'); -// --- Lecture des arguments CLI --- +// --- Lecture des arguments CLI ---j const argv = minimist(process.argv.slice(2), { default: { gameWsPort: 3000, @@ -63,6 +64,10 @@ const monitoringWSS = new WebSocket.Server({ port: MONITOR_PORT }, () => { function sendMonitorEvent(event) { const payload = JSON.stringify(event); + // Sauvegarde systématique de l'événement dans le buffer BDD + // On considère que tout ce qui part au monitoring est un événement "officiel" du jeu + bufferGameEvents(SENSOR_ID, event); + monitoringWSS.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(payload); @@ -92,7 +97,10 @@ const parser = arduinoPort.pipe(new ReadlineParser()); parser.on("data", line => { const cleanLine = line.trim(); + // --- SAUT --- if (cleanLine.includes("JUMP")) { + const jumpEvent = { type: "input", action: "jump", source: "arduino" }; + // Envoi au jeu wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { @@ -100,7 +108,27 @@ parser.on("data", line => { } }); - sendMonitorEvent({ type: "input", action: "jump" }); + // Envoi au monitoring (+ Sauvegarde BDD via sendMonitorEvent) + sendMonitorEvent(jumpEvent); + } + + // --- VITESSE (Nouveau) --- + if (cleanLine.startsWith("V")) { + const speedVal = parseInt(cleanLine.substring(1)); + + if (!isNaN(speedVal)) { + const speedEvent = { type: "speed", speed: speedVal, source: "arduino" }; + + // Envoi au jeu (pour modifier la vitesse des obstacles) + wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify({ type: "speed", value: speedVal })); + } + }); + + // Envoi au monitoring (+ Sauvegarde BDD via sendMonitorEvent) + sendMonitorEvent(speedEvent); + } } }); @@ -120,6 +148,8 @@ wss.on("connection", ws => { console.log("Client connecté au serveur JEU"); ws.on("close", () => { + // Événement système : fin de session + bufferGameEvents(SENSOR_ID, { type: "system", event: "client_disconnected" }); currentSessionScore = 0; }); @@ -127,25 +157,31 @@ wss.on("connection", ws => { try { const data = JSON.parse(msg); + // --- INPUT DU FRONTEND (Clavier/Touch) --- if (data.type === "input") { sendMonitorEvent({ type: "input", action: data.action, + source: "frontend" }); } // --- SCORE --- if (data.type === "score") { - + + // On sauvegarde le score comme un événement sendMonitorEvent({ type: "score", score: data.score, }); - // Fin de partie + // Logique de Fin de partie / Session if (data.score === 0) { if (currentSessionScore >= MIN_SCORE_TO_SAVE) { + // Sauvegarde de la "Run" globale bufferGameRun(SENSOR_ID, currentSessionScore, currentSessionDeaths); + // Sauvegarde de l'événement de fin + bufferGameEvents(SENSOR_ID, { type: "game_end", finalScore: currentSessionScore }); } currentSessionScore = 0; @@ -176,11 +212,12 @@ wss.on("connection", ws => { }); }); - monitoringWSS.on("connection", ws => { console.log("Monitor server connected!"); + // Log de connexion monitoring + bufferGameEvents(SENSOR_ID, { type: "system", event: "monitor_connected" }); ws.on("close", () => { console.log("Monitor server disconnected!"); }); -}); +}); \ No newline at end of file