diff --git a/.gitignore b/.gitignore index c2658d7d1b31848c3b71960543cb0368e56cd4c7..4e23707ee32bde89062f0714b961b236cefdc8d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +.env \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..0de1a99ccfcf27772ed50d561ea3be408c978331 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: jump_dash_db + restart: always + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + ports: + - "${DB_PORT}:5432" + volumes: + - db_data:/var/lib/postgresql/data + +volumes: + db_data: \ No newline at end of file diff --git a/game/server/db/db.js b/game/server/db/db.js new file mode 100644 index 0000000000000000000000000000000000000000..ca3c1314e51327449b62cc26f377fde149f8eb19 --- /dev/null +++ b/game/server/db/db.js @@ -0,0 +1,99 @@ +const path = require('path'); +// Chargement des variables d'environnement depuis la racine du projet +require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') }); + +const { Pool } = require('pg'); + +// --- Configuration de la Connexion BDD --- +// Utilisation des variables definies dans le fichier .env pour la securite +const pool = new Pool({ + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: process.env.DB_PORT || 5432, + host: 'localhost', +}); + +// --- Configuration du Buffer d'Ecriture --- +// Stockage temporaire des parties terminees avant insertion en base +let writeBuffer = []; +// 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."); + } 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) +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({ + sensorId: sensorId, + score: score, + deaths: deaths, + 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; + + // Copie locale du buffer et reinitialisation immediate pour ne pas bloquer les nouvelles entrees + const currentBatch = [...writeBuffer]; + writeBuffer = []; + + console.log(`Traitement du lot : sauvegarde de ${currentBatch.length} entrées en base...`); + + const client = await pool.connect(); + try { + // Debut de la transaction SQL + await client.query('BEGIN'); + + for (const run of currentBatch) { + 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] + ); + } + + // Validation de la transaction + await client.query('COMMIT'); + console.log("Sauvegarde BDD reussie."); + } catch (e) { + // Annulation en cas d'erreur pour garantir l'integrite des donnees + await client.query('ROLLBACK'); + console.error("Echec de la sauvegarde BDD :", e.message); + } finally { + // Liberation du client du pool + client.release(); + } +} + +// Lancement du timer pour le vidage automatique du buffer +setInterval(flushBuffer, FLUSH_INTERVAL); + +module.exports = { initDB, bufferGameRun }; \ No newline at end of file diff --git a/game/server/package-lock.json b/game/server/package-lock.json index 68dd97b241f37ae353c00dd7b8b1220a08e468b3..9db677b2d484247131a18630e23e9ef3ca6a5572 100644 --- a/game/server/package-lock.json +++ b/game/server/package-lock.json @@ -5,7 +5,9 @@ "packages": { "": { "dependencies": { + "dotenv": "^17.2.3", "express": "^5.1.0", + "pg": "^8.16.3", "serialport": "^13.0.0", "ws": "^8.18.3" } @@ -386,6 +388,18 @@ "node": ">= 0.8" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -814,6 +828,134 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1089,6 +1231,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1165,6 +1316,15 @@ "optional": true } } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } } } } diff --git a/game/server/package.json b/game/server/package.json index 9a920c41c10c6bd6ffadb47e0c584f7fc9467ed2..56c9042d25ac60ac6e012656f1eee1cccc4d4557 100644 --- a/game/server/package.json +++ b/game/server/package.json @@ -1,6 +1,8 @@ { "dependencies": { + "dotenv": "^17.2.3", "express": "^5.1.0", + "pg": "^8.16.3", "serialport": "^13.0.0", "ws": "^8.18.3" } diff --git a/game/server/server.js b/game/server/server.js index 815b65dc776bcec17db9384361a98c8f54b59dc2..97ebfc6b2ee8a152ec8a63c83b56f83711fda9db 100644 --- a/game/server/server.js +++ b/game/server/server.js @@ -1,76 +1,60 @@ +// Chargement de la configuration sécurisée (.env) +require('dotenv').config({ path: '../../.env' }); + const express = require('express'); const { SerialPort, ReadlineParser } = require('serialport'); const WebSocket = require('ws'); +// Import du module de base de données +const { initDB, bufferGameRun } = require('./db/db'); const app = express(); const port = 3000; +// --- Configuration Métier --- +const SENSOR_ID = "ttyACM0"; // Identifiant du capteur pour la BDD +const MIN_SCORE_TO_SAVE = 2; // Seuil pour éviter le spam en BDD -// ---- Express Serve Static Files ---- -const server = app.listen(port, () => { - console.log(`Server running at http://localhost:${port}`); -}); - -// ---- WebSocket Server ---- -const wss = new WebSocket.Server({ server }); -wss.on("connection", ws => { - console.log("Client connected"); - - ws.on("message", msg => { - try { - const data = JSON.parse(msg); - - if(data.type == "input") { - console.log("User input: ", data.action); - sendToArduino(String(data.action).toUpperCase()) - } +// --- Variables de Session --- +let currentSessionScore = 0; +let currentSessionDeaths = 0; - if (data.type === "score") { - console.log("Score update:", data.score); +// Initialisation BDD au démarrage +initDB(); - // send to Arduino - sendToArduino(`S${data.score}`); - } - - if (data.type === "death") { - console.log("Death update:", data.deaths); - - // send to Arduino - sendToArduino(`D${data.deaths}`); - } - } catch (e) { - console.error("Invalid message", e); - } - }); +// --- Serveur HTTP --- +const server = app.listen(port, () => { + console.log(`Serveur démarré sur http://localhost:${port}`); }); +// --- Serveur WebSocket --- +const wss = new WebSocket.Server({ server }); -// ---- SerialPort Setup ---- +// --- Configuration Ports Série (Architecture Répéteur) --- -// A. Le Vrai Port (Hardware) +// 1. Le Vrai Port (Pour le Jeu) +// Connexion directe au matériel pour zéro latence const arduinoPort = new SerialPort({ path: "/dev/ttyACM0", baudRate: 9600, + autoOpen: true }); -// B. Le Port Pont (Pour le Monitor +// 2. Le Pont Virtuel (Pour le Moniteur) +// Copie du trafic pour les graphiques const bridgePort = new SerialPort({ path: "/dev/ttyBRIDGE", baudRate: 9600, + autoOpen: true }); const parser = arduinoPort.pipe(new ReadlineParser()); -// When Arduino sends a line: +// --- Réception Données (Arduino -> PC) --- parser.on("data", line => { - - const cleanLine = line.trim(); - console.log("Serial:", cleanLine); - - // 1. Pour le JEU (WebSocket) - // On utilise 'includes' pour être plus robuste si socat colle des caractères + const cleanLine = line.trim(); + + // 1. Traitement Jeu (Détection du saut) if (cleanLine.includes("JUMP")) { - // broadcast jump to all browsers wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ action: "jump" })); @@ -78,28 +62,71 @@ parser.on("data", line => { }); } - // 2. Pour le MONITEUR (Le Répéteur) - // On copie ce qu'on reçoit vers le tuyau virtuel + // 2. Copie vers Moniteur (Miroir) if (bridgePort.isOpen) { bridgePort.write(cleanLine + "\n"); } }); -// --- Utility Functions --- +// --- Émission Données (PC -> Arduino) --- function sendToArduino(message) { const msg = message + "\n"; - - // Écriture sur le vrai Arduino (pour l'affichage) + + // Envoi au matériel réel (Affichage) if (arduinoPort.isOpen) { - arduinoPort.write(message + "\n", err => { - if (err) return console.error("Serial write error:", err); + arduinoPort.write(msg, err => { + if (err) console.error("Erreur écriture Arduino :", err); }); } - - // Écriture sur le Pont (pour que le Monitor voie aussi les scores) + + // Envoi au pont virtuel (Logs Moniteur) if (bridgePort.isOpen) { bridgePort.write(msg, err => { - if (err) return console.error("Bridge write error:", err); + if (err) console.error("Erreur écriture Bridge :", err); }); } -} \ No newline at end of file +} + +// --- WebSocket & Logique BDD --- +wss.on("connection", ws => { + console.log("Client connecté"); + + ws.on("close", () => { + currentSessionScore = 0; + }); + + ws.on("message", msg => { + try { + const data = JSON.parse(msg); + + if(data.type == "input") { + sendToArduino(String(data.action).toUpperCase()); + } + + if (data.type === "score") { + // Détection Fin de Partie (Signal Restart = 0) + if (data.score === 0) { + // Sauvegarde si le score précédent était pertinent + if (currentSessionScore >= MIN_SCORE_TO_SAVE) { + console.log(`Fin de session. Score: ${currentSessionScore}. Ajout au buffer.`); + bufferGameRun(SENSOR_ID, currentSessionScore, currentSessionDeaths); + } + currentSessionScore = 0; + currentSessionDeaths = 0; + } else { + currentSessionScore = data.score; + } + + sendToArduino(`S${data.score}`); + } + + if (data.type === "death") { + currentSessionDeaths = data.deaths; + sendToArduino(`D${data.deaths}`); + } + + } catch (e) { + console.error("Erreur WS :", e); + } + }); +}); \ No newline at end of file