From 429e397f207902c4e7297185ea9a38bb42f383ef Mon Sep 17 00:00:00 2001 From: Massiles Ghernaout <749-gm213204@users.noreply.www-apps.univ-lehavre.fr> Date: Sun, 30 Nov 2025 14:38:55 +0100 Subject: [PATCH] Architecture reveiw: Instead of using multiple socat commands for the data mirroring between the diffrent virtual serial bus links, we will rather use only the arduino serial bus and then expose another websocket server for the monitoring servers. This way it is much easier to scale and the code is still decoupled between the game server and the monitor server logic. We can rollback, change the game and the monitor server event formats independently. We can also remove the monitor server entirely, but I've choosed to leave it there in case we want to enrich the data before sending it to the monitor client, besides we can imaging a local caching mechanism for the monitor server or another spin of our pgsql db for it as well, this is why having a server is still a good idea. --- game/server/package-lock.json | 56 ++++++------ game/server/package.json | 1 + game/server/server.js | 140 ++++++++++++++++++------------ monitor/client/src/Dashboard.tsx | 5 +- monitor/server/monitor-arduino.js | 115 ++++++++---------------- 5 files changed, 158 insertions(+), 159 deletions(-) diff --git a/game/server/package-lock.json b/game/server/package-lock.json index 9db677b..2e06169 100644 --- a/game/server/package-lock.json +++ b/game/server/package-lock.json @@ -4,9 +4,11 @@ "requires": true, "packages": { "": { + "name": "server", "dependencies": { "dotenv": "^17.2.3", "express": "^5.1.0", + "minimist": "^1.2.8", "pg": "^8.16.3", "serialport": "^13.0.0", "ws": "^8.18.3" @@ -265,23 +267,27 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bytes": { @@ -654,15 +660,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/inherits": { @@ -741,6 +751,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -833,6 +852,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -1024,22 +1044,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/raw-body/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/game/server/package.json b/game/server/package.json index 56c9042..4450e1d 100644 --- a/game/server/package.json +++ b/game/server/package.json @@ -3,6 +3,7 @@ "dotenv": "^17.2.3", "express": "^5.1.0", "pg": "^8.16.3", + "minimist": "^1.2.8", "serialport": "^13.0.0", "ws": "^8.18.3" } diff --git a/game/server/server.js b/game/server/server.js index 97ebfc6..532630e 100644 --- a/game/server/server.js +++ b/game/server/server.js @@ -1,95 +1,110 @@ // Chargement de la configuration sécurisée (.env) require('dotenv').config({ path: '../../.env' }); +const minimist = require('minimist'); 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; +// --- Lecture des arguments CLI --- +const argv = minimist(process.argv.slice(2), { + default: { + httpPort: 3000, + monitorPort: 3001, + sensorId: "ttyACM0", + baud: 9600, + minScore: 2 + } +}); -// --- 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 +// --- Configuration CLI --- +const HTTP_PORT = argv.httpPort; +const MONITOR_PORT = argv.monitorPort; +const SENSOR_ID = argv.sensorId; +const MIN_SCORE_TO_SAVE = argv.minScore; +const BAUD_RATE = argv.baud; + +console.log("Configuration utilisée :", { + HTTP_PORT, + MONITOR_PORT, + SENSOR_ID, + BAUD_RATE, + MIN_SCORE_TO_SAVE +}); // --- Variables de Session --- let currentSessionScore = 0; let currentSessionDeaths = 0; -// Initialisation BDD au démarrage +// Initialisation BDD initDB(); +const app = express(); + // --- Serveur HTTP --- -const server = app.listen(port, () => { - console.log(`Serveur démarré sur http://localhost:${port}`); +const server = app.listen(HTTP_PORT, () => { + console.log(`Serveur HTTP + WebSocket Jeu sur http://localhost:${HTTP_PORT}`); }); -// --- Serveur WebSocket --- +// --- Serveur WebSocket (JEU) --- const wss = new WebSocket.Server({ server }); -// --- Configuration Ports Série (Architecture Répéteur) --- - -// 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 +// --- Serveur WebSocket (MONITORING) --- +const monitoringWSS = new WebSocket.Server({ port: MONITOR_PORT }, () => { + console.log(`Serveur WebSocket Monitoring sur ws://localhost:${MONITOR_PORT}`); }); -// 2. Le Pont Virtuel (Pour le Moniteur) -// Copie du trafic pour les graphiques -const bridgePort = new SerialPort({ - path: "/dev/ttyBRIDGE", - baudRate: 9600, +// Fonction utilitaire pour envoyer des événements au monitoring +function sendMonitorEvent(event) { + const payload = JSON.stringify(event); + + monitoringWSS.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(payload); + } + }); +} + +// --- Configuration Port Série --- +const arduinoPort = new SerialPort({ + path: `/dev/${SENSOR_ID}`, + baudRate: BAUD_RATE, autoOpen: true }); const parser = arduinoPort.pipe(new ReadlineParser()); -// --- Réception Données (Arduino -> PC) --- +// --- Réception Données Arduino --- parser.on("data", line => { const cleanLine = line.trim(); - - // 1. Traitement Jeu (Détection du saut) + if (cleanLine.includes("JUMP")) { + // Envoi au jeu wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ action: "jump" })); } }); - } - // 2. Copie vers Moniteur (Miroir) - if (bridgePort.isOpen) { - bridgePort.write(cleanLine + "\n"); + sendMonitorEvent({ type: "input", action: "jump" }); } }); -// --- Émission Données (PC -> Arduino) --- +// --- Envoi Arduino --- function sendToArduino(message) { const msg = message + "\n"; - - // Envoi au matériel réel (Affichage) + if (arduinoPort.isOpen) { arduinoPort.write(msg, err => { if (err) console.error("Erreur écriture Arduino :", err); }); } - - // Envoi au pont virtuel (Logs Moniteur) - if (bridgePort.isOpen) { - bridgePort.write(msg, err => { - if (err) console.error("Erreur écriture Bridge :", err); - }); - } } -// --- WebSocket & Logique BDD --- +// --- WebSocket JEU --- wss.on("connection", ws => { - console.log("Client connecté"); + console.log("Client connecté au serveur JEU"); ws.on("close", () => { currentSessionScore = 0; @@ -99,29 +114,46 @@ wss.on("connection", ws => { try { const data = JSON.parse(msg); - if(data.type == "input") { - sendToArduino(String(data.action).toUpperCase()); + if (data.type === "input") { + sendMonitorEvent({ + type: "input", + action: data.action, + }); } + // --- SCORE --- if (data.type === "score") { - // Détection Fin de Partie (Signal Restart = 0) + + sendMonitorEvent({ + type: "score", + score: data.score, + }); + + // Fin de partie 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; + if (currentSessionScore >= MIN_SCORE_TO_SAVE) { + bufferGameRun(SENSOR_ID, currentSessionScore, currentSessionDeaths); + } + + currentSessionScore = 0; + currentSessionDeaths = 0; + } else { - currentSessionScore = data.score; + currentSessionScore = data.score; } sendToArduino(`S${data.score}`); } + // --- DEATHS --- if (data.type === "death") { currentSessionDeaths = data.deaths; + + sendMonitorEvent({ + type: "death", + deaths: data.deaths, + }); + sendToArduino(`D${data.deaths}`); } @@ -129,4 +161,4 @@ wss.on("connection", ws => { console.error("Erreur WS :", e); } }); -}); \ No newline at end of file +}); diff --git a/monitor/client/src/Dashboard.tsx b/monitor/client/src/Dashboard.tsx index 9370856..962f29f 100644 --- a/monitor/client/src/Dashboard.tsx +++ b/monitor/client/src/Dashboard.tsx @@ -5,7 +5,7 @@ import Charts from "./Charts"; // Define the shape of the messages coming from WebSocket type WSData = - | { action: "jump" } + | { type: "input"; action: "jump" } | { type: "score"; score: number } | { type: "death"; deaths: number }; @@ -24,13 +24,14 @@ export default function Dashboard() { const wsRef = useRef(null); useEffect(() => { - const ws = new WebSocket("ws://localhost:8000"); + const ws = new WebSocket("ws://localhost:8080"); wsRef.current = ws; ws.onopen = () => setConnected(true); ws.onclose = () => setConnected(false); ws.onmessage = (msg) => { + console.log(msg.data); const data: WSData = JSON.parse(msg.data); // raw log diff --git a/monitor/server/monitor-arduino.js b/monitor/server/monitor-arduino.js index 6ebeea0..26d08a5 100644 --- a/monitor/server/monitor-arduino.js +++ b/monitor/server/monitor-arduino.js @@ -1,95 +1,56 @@ -#!/usr/bin/env node +const WebSocket = require('ws'); +const minimist = require('minimist'); -const { SerialPort, ReadlineParser } = require("serialport"); -const express = require("express"); -const http = require("http"); -const WebSocket = require("ws"); +// ------- CLI ARGUMENTS ------- +// Example: +// node monitor-arduino.js --remote=wss://remote.example.com --port=8080 -// --------------------------- -// COMMAND LINE ARGUMENTS -// --------------------------- -// -// Usage: -// node monitor-arduino.js --serial=/dev/ttyUSB0 --baud=9600 --http=8000 -// +const args = minimist(process.argv.slice(2)); -const args = require("minimist")(process.argv.slice(2), { - string: ["serial", "baud", "http"], -}); +const REMOTE_WS_URL = args.remote || "ws://localhost:9000"; +const LOCAL_PORT = args.port || 8080; + +console.log("Remote WebSocket URL:", REMOTE_WS_URL); +console.log("Local server port:", LOCAL_PORT); -const PORT_NAME = args.serial; -const BAUD = parseInt(args.baud, 10); -const HTTP_PORT = parseInt(args.http, 10); +// ------- CONNECT TO REMOTE SERVER ------- +const remoteSocket = new WebSocket(REMOTE_WS_URL); -// --------------------------- -// SERIAL PORT SETUP -// --------------------------- -const port = new SerialPort({ - path: PORT_NAME, - baudRate: BAUD, +remoteSocket.on('open', () => { + console.log("Connected to remote WebSocket server:", REMOTE_WS_URL); }); -const parser = port.pipe(new ReadlineParser({ delimiter: "\n" })); +remoteSocket.on('error', (err) => { + console.error("Remote WS error:", err); +}); -port.on("open", () => { - console.log(`Listening to Arduino on ${PORT_NAME} at ${BAUD} baud`); +// ------- CREATE LOCAL SERVER ------- +const localWSS = new WebSocket.Server({ port: LOCAL_PORT }, () => { + console.log(`Local WebSocket server running on ws://localhost:${LOCAL_PORT}`); }); -// --------------------------- -// WEBSERVER + WEBSOCKET -// --------------------------- -const app = express(); -const server = http.createServer(app); -const wss = new WebSocket.Server({ server }); +let clients = []; -wss.on("connection", (ws) => { - console.log("WebSocket client connected"); -}); +// Handle clients connecting to local proxy +localWSS.on('connection', (socket) => { + console.log("Client connected"); + clients.push(socket); -// broadcast helper -function broadcast(obj) { - const msg = JSON.stringify(obj); - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(msg); - } + socket.on('close', () => { + console.log("Client disconnected"); + clients = clients.filter(s => s !== socket); }); -} - -// --------------------------- -// PARSE SERIAL AND FORWARD -// --------------------------- -parser.on("data", (raw) => { - const line = raw.trim(); - console.log("Serial:", line); +}); - if (line === "JUMP") { - broadcast({ type: "input", action: "jump" }); - return; - } +// ------- FORWARD REMOTE → LOCAL CLIENTS ------- +remoteSocket.on('message', (msg) => { - if (line.startsWith("S")) { - const value = parseInt(line.substring(1), 10); - if (!isNaN(value)) { - broadcast({ type: "score", score: value }); - } - return; - } + const text = msg.toString(); // convert Buffer → UTF-8 string + console.log("Received from remote:", text); - if (line.startsWith("D")) { - const value = parseInt(line.substring(1), 10); - if (!isNaN(value)) { - broadcast({ type: "death", deaths: value }); + clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(text); // send text, not binary } - return; - } - - broadcast({ type: "unknown", raw: line }); -}); - -// --------------------------- -// START SERVER -// --------------------------- -server.listen(HTTP_PORT, () => { - console.log(`HTTP/WebSocket server running at http://localhost:${HTTP_PORT}`); + }); }); -- GitLab