1
0
forked from jchomaz/Vulture

36 Commits

Author SHA1 Message Date
184a6ac600 config : corrige ip buzzer 2026-01-26 17:32:29 +01:00
9a4a2cb6ad Change le port exposé de l'app 2026-01-26 17:13:08 +01:00
3a458be33d feat: Refactor VApp container to a development build and update its exposed port mapping in the run script. 2026-01-26 17:13:08 +01:00
4e0f34f75c chore: Update buzzer IP addresses in network configuration. 2026-01-26 17:13:08 +01:00
19fe61f077 feat: Introduce Quadlet units for dev/prod VApp containers and a custom network, update existing container configurations, and enhance documentation. 2026-01-26 17:13:07 +01:00
b86909c744 Créer 2 scripts de lancement un pour le dev en local l'autre pour la prod 2026-01-26 17:13:07 +01:00
4e57c70b3c gestion d'une config pour les ip si prod ou dev 2026-01-26 17:13:07 +01:00
da932bdcb3 Lancement en mode network plutot que pod 2026-01-26 17:13:02 +01:00
6026bfb7ff Quadlet (WiP) 2026-01-26 17:09:46 +01:00
28a05c2104 scripts de gestion 2026-01-26 17:09:46 +01:00
b9a2c53032 Correction des options de lancement 2026-01-26 17:09:46 +01:00
c73322a67a Mise à jour des paquets 2026-01-23 18:52:04 +01:00
5900b1faa1 Ajout des boutons de simulation de buzzer et agrandissement de la fenêtre 2026-01-23 18:50:58 +01:00
af58e9c30d Agrandissement de la fenêtre de debug 2026-01-23 18:49:50 +01:00
bc8846d9eb Nouveau player video du game display 2026-01-23 18:48:41 +01:00
5c16468157 Page d'overlay qui masque le player 2026-01-23 18:48:13 +01:00
911671c653 Nouvelle page qui s'affiche en popup sur la page de controle 2026-01-23 18:47:44 +01:00
cd540698a1 Modification de l'équipe Orange en jaune 2026-01-23 18:47:01 +01:00
2a28526cb9 Modification de l'équipe Orange en jaune 2026-01-23 18:45:55 +01:00
6666874913 Modification du controle manuel des score et suppression des 16 bouttons 2026-01-23 18:44:35 +01:00
814c3d0e68 Ajout du popup quand les équipes buzz 2026-01-23 18:43:56 +01:00
911497ab1d Ajout des scores dans les pastilles d'équipes et de le mise à jour en live des scores 2026-01-23 18:42:42 +01:00
905da933dc Modification du score-manager 2026-01-23 18:40:44 +01:00
0186a0a83e Modification du buzzer-manager 2026-01-23 18:40:23 +01:00
da929ecb89 Merge pull request 'container-integration' (#1) from lol/Vulture:container-integration into main
Reviewed-on: jchomaz/Vulture#1
2025-11-24 17:47:13 +01:00
4ab0cca1b2 (conf) VApp : configuration de la bonne adresse du serveur mqtt 2025-11-24 17:40:00 +01:00
5401f416e7 (chore) VNode: ignore les json dans le répertoire des scores 2025-11-16 19:07:45 +01:00
856b90567d (config) VNode : Utilise localhost comme adresse du serveur mqtt
On est sur le même pod.
Voir si besoin de faire une variable d'env ou autre si besoin de
spécifier pour le dev
2025-11-16 19:05:59 +01:00
bb791ed2f4 (feat) VContainers : build VNode 2025-11-16 19:05:59 +01:00
768f42dff4 (feat) VContainers : build VApp 2025-11-16 19:05:59 +01:00
bc807f9c7b (feat) VContainers : Fichier de configuration pour nanomq 2025-11-16 19:05:54 +01:00
8d980a90c8 (doc) VContainers : document création et lancement de container 2025-11-16 19:05:49 +01:00
e8607c78e9 (fix) Vapp : Fuck***g caseless filesystem 2025-11-16 19:05:49 +01:00
1ff31e0991 (fix) VNode: portable path 2025-11-16 19:05:42 +01:00
3e29bfca18 (chore) VApp : Update dependencies 2025-11-16 19:05:41 +01:00
56c6a744c7 (chore) All: Ajoute .gitignore générique pour js/vue/... 2025-11-16 19:05:28 +01:00
42 changed files with 3367 additions and 1957 deletions

142
.gitignore vendored Normal file
View File

@@ -0,0 +1,142 @@
# Score files
/VNode/services/game/score/*.json
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
<head> <meta charset="UTF-8"> <link rel="icon" href="/favicon.ico"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Brain Blast</title> <head> <meta charset="UTF-8"> <link rel="icon" href="/favicon.ico"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Brain Blast</title>
<script src="/config.js"></script>
</head> </head>
<body> <div id="app"></div> <script type="module" src="/src/main.js"></script> <body> <div id="app"></div> <script type="module" src="/src/main.js"></script>
</body> </body>

3590
VApp/package-lock.json generated

File diff suppressed because it is too large Load Diff

7
VApp/public/config.js Normal file
View File

@@ -0,0 +1,7 @@
window.APP_CONFIG = {
mqttBrokerUrl: 'ws://192.168.73.252:9001',
redBuzzerIP: '192.168.73.40',
blueBuzzerIP: '192.168.73.41',
orangeBuzzerIP: '192.168.73.42',
greenBuzzerIP: '192.168.73.43'
};

View File

Before

Width:  |  Height:  |  Size: 766 KiB

After

Width:  |  Height:  |  Size: 766 KiB

View File

@@ -0,0 +1,146 @@
<template>
<v-dialog v-model="dialog" persistent max-width="800" height="500">
<v-card dark rounded="xl">
<v-card-title :style="{ backgroundColor: buzzerColor }" class="headline text-center justify-center">
<v-icon color="background" dark large left size="70">mdi-alarm-light</v-icon>
</v-card-title>
<v-card-text :style="{ color: buzzerColor }" class="text-style">
L'équipe {{ buzzerTeam }} a buzzé !
</v-card-text>
<v-card-actions class="justify-center pa-0 ma-0" style="height: 100px; gap: 0;">
<v-btn
class="refuse-btn ma-0"
tile
rounded="0"
height="100%"
width="50%"
@click="refuse">
<v-icon left size="40">mdi-close-circle</v-icon>
<span style="font-size: 20px; padding-left: 10px;">Refuser</span>
</v-btn>
<v-btn
class="validate-btn ma-0"
tile
rounded="0"
height="100%"
width="50%"
@click="validate">
<v-icon left size="40">mdi-check-circle</v-icon>
<span style="font-size: 20px; padding-left: 10px;">Valider (+1)</span>
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import mqtt from 'mqtt';
import config from '@/config.js';
import { useTheme } from 'vuetify';
const theme = useTheme();
const dialog = ref(false);
const buzzerTeam = ref('');
const buzzerColor = ref('');
const client = mqtt.connect(config.mqttBrokerUrl);
// Map hex colors to team names if needed, or just use the color directly
function getTeamNameFromColor(color) {
const c = color.toUpperCase();
const colors = theme.current.value.colors;
console.log('Received Color:', c);
console.log('Comparing against:', colors.RedBuzzer.toUpperCase(), colors.BlueBuzzer.toUpperCase(), colors.YellowBuzzer.toUpperCase(), colors.GreenBuzzer.toUpperCase());
if (c === colors.RedBuzzer.toUpperCase()) return 'rouge';
if (c === colors.BlueBuzzer.toUpperCase()) return 'bleue';
if (c === colors.YellowBuzzer.toUpperCase()) return 'jaune';
if (c === colors.GreenBuzzer.toUpperCase()) return 'verte';
return color; // Fallback
}
function getTeamKeyFromColor(color) {
const c = color.toUpperCase();
const colors = theme.current.value.colors;
if (c === colors.RedBuzzer.toUpperCase()) return 'Red';
if (c === colors.BlueBuzzer.toUpperCase()) return 'Blue';
if (c === colors.YellowBuzzer.toUpperCase()) return 'Yellow';
if (c === colors.GreenBuzzer.toUpperCase()) return 'Green';
return null;
}
onMounted(() => {
client.on('connect', () => {
console.log('BuzzerValidation: Connected');
client.subscribe('vulture/buzzer/status');
});
client.on('message', (topic, message) => {
if (topic === 'vulture/buzzer/status') {
try {
const data = JSON.parse(message.toString());
if (data.status === 'blocked') {
buzzerColor.value = data.color;
buzzerTeam.value = getTeamNameFromColor(data.color);
dialog.value = true;
} else if (data.status === 'unblocked') {
// Optional: auto-close if unblocked from elsewhere
dialog.value = false;
}
} catch (e) {
console.error('Error parsing buzzer status:', e);
}
}
});
});
function unlockBuzzers() {
client.publish('vulture/buzzer/unlock','0');
}
function validate() {
const teamKey = getTeamKeyFromColor(buzzerColor.value);
if (teamKey) {
const payload = { [teamKey]: "+1" };
client.publish('game/score/update', JSON.stringify(payload));
}
// Add a small delay before unlocking to ensure the score update is processed
setTimeout(() => {
unlockBuzzers();
}, 100);
dialog.value = false;
}
function refuse() {
unlockBuzzers();
dialog.value = false;
}
</script>
<style scoped>
.headline {
font-weight: bold;
}
.text-style {
font-size: 45px!important;
font-weight: 600;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
height: 100%; /* Ensure it takes full height of the container if possible, or substantial height */
}
.validate-btn {
background-color: rgb(var(--v-theme-success),1);
color: rgb(var(--v-theme-background),1);
}
.refuse-btn {
background-color: rgb(var(--v-theme-error),1);
color: rgb(var(--v-theme-background),1);
}
</style>

View File

@@ -4,111 +4,152 @@
<v-icon left class="white--text pr-5 pl-2" size="40">mdi-calculator-variant</v-icon> <v-icon left class="white--text pr-5 pl-2" size="40">mdi-calculator-variant</v-icon>
Gestion des scores Gestion des scores
</v-card-title> </v-card-title>
<v-container class="text-center"> <v-container class="text-center pt-8">
<v-row justify="center"> <!-- Team Lines -->
<v-col cols="4" sm="6" md="3"> <v-row v-for="(team, color) in scores" :key="color" align="center" justify="center" class="mb-2">
<mqtt-button width="120" height="60" class="btn red xs12 sm6 md3" topic="game/score/update" message='{"Red": "-2"}'> <!-- Icon/Label -->
<v-icon left size="40">mdi-minus-box-multiple</v-icon> <v-col cols="2" class="d-flex justify-center">
</mqtt-button> <v-icon :color="getTeamColor(color)" size="40">mdi-circle</v-icon>
</v-col> </v-col>
<v-col cols="4" sm="6" md="3">
<mqtt-button width="120" height="60" class="btn red card xs12 sm6 md3" topic="game/score/update" message='{"Red": "-1"}'> <!-- Total Score Input -->
<v-icon left size="40">mdi-minus-box</v-icon> <v-col cols="5">
</mqtt-button> <v-text-field
</v-col> v-model.number="team.Total"
<v-col cols="4" sm="6" md="3"> label="Total"
<mqtt-button width="120" height="60" class="btn red card xs12 sm6 md3" topic="game/score/update" message='{"Red": "+1"}'> type="number"
<v-icon left size="40">mdi-plus-box</v-icon> variant="outlined"
</mqtt-button> density="compact"
</v-col> hide-details
<v-col cols="4" sm="6" md="3"> prepend-inner-icon="mdi-minus"
<mqtt-button width="120" height="60" class="btn red card xs12 sm6 md3 " topic="game/score/update" message='{"Red": "+2"}'> append-inner-icon="mdi-plus"
<v-icon left size="40">mdi-plus-box-multiple</v-icon> :color="getTeamColor(color)"
</mqtt-button> :base-color="getTeamColor(color)"
</v-col> @click:prepend-inner="changeScore(color, 'Total', -1)"
<v-col cols="4" sm="6" md="3"> @click:append-inner="changeScore(color, 'Total', 1)"
<mqtt-button width="120" height="60" class="btn blue card xs12 sm6 md3 " topic="game/score/update" message='{"Blue": "-2"}'> @update:model-value="updateScore(color)"
<v-icon left size="40">mdi-minus-box-multiple</v-icon> class="centered-input"
</mqtt-button> readonly
</v-col> ></v-text-field>
<v-col cols="12" sm="6" md="3"> </v-col>
<mqtt-button width="120" height="60" class="btn blue card xs12 sm6 md3 " topic="game/score/update" message='{"Blue": "-1"}'>
<v-icon left size="40">mdi-minus-box</v-icon> <!-- Round Score Input -->
</mqtt-button> <v-col cols="5">
</v-col> <v-text-field
<v-col cols="4" sm="6" md="3"> v-model.number="team.Round"
<mqtt-button width="120" height="60" class="btn blue card xs12 sm6 md3 " topic="game/score/update" message='{"Blue": "+1"}'> label="Manche"
<v-icon left size="40">mdi-plus-box</v-icon> type="number"
</mqtt-button> variant="outlined"
</v-col> density="compact"
<v-col cols="12" sm="6" md="3"> hide-details
<mqtt-button width="120" height="60" class="btn blue card xs12 sm6 md3 " topic="game/score/update" message='{"Blue": "+2"}'> prepend-inner-icon="mdi-minus"
<v-icon left size="40">mdi-plus-box-multiple</v-icon> append-inner-icon="mdi-plus"
</mqtt-button> :color="getTeamColor(color)"
</v-col> :base-color="getTeamColor(color)"
<v-col cols="4" sm="6" md="3"> @click:prepend-inner="changeScore(color, 'Round', -1)"
<mqtt-button width="120" height="60" class="btn yellow card xs12 sm6 md3 " topic="game/score/update" message='{"Yellow": "-2"}'> @click:append-inner="changeScore(color, 'Round', 1)"
<v-icon left size="40">mdi-minus-box-multiple</v-icon> @update:model-value="updateScore(color)"
</mqtt-button> class="centered-input"
</v-col> readonly
<v-col cols="12" sm="6" md="3"> ></v-text-field>
<mqtt-button width="120" height="60" class="btn yellow card xs12 sm6 md3 " topic="game/score/update" message='{"Yellow": "-1"}'> </v-col>
<v-icon left size="40">mdi-minus-box</v-icon>
</mqtt-button>
</v-col>
<v-col cols="4" sm="6" md="3">
<mqtt-button width="120" height="60" class="btn yellow card xs12 sm6 md3 " topic="game/score/update" message='{"Yellow": "+1"}'>
<v-icon left size="40">mdi-plus-box</v-icon>
</mqtt-button>
</v-col>
<v-col cols="12" sm="6" md="3">
<mqtt-button width="120" height="60" class="btn yellow card xs12 sm6 md3 " topic="game/score/update" message='{"Yellow": "+2"}'>
<v-icon left size="40">mdi-plus-box-multiple</v-icon>
</mqtt-button>
</v-col>
<v-col cols="4" sm="6" md="3">
<mqtt-button width="120" height="60" class="btn green card xs12 sm6 md3 " topic="game/score/update" message='{"Green": "-2"}'>
<v-icon left size="40">mdi-minus-box-multiple</v-icon>
</mqtt-button>
</v-col>
<v-col cols="12" sm="6" md="3">
<mqtt-button width="120" height="60" class="btn green card xs12 sm6 md3 " topic="game/score/update" message='{"Green": "-1"}'>
<v-icon left size="40">mdi-minus-box</v-icon>
</mqtt-button>
</v-col>
<v-col cols="4" sm="6" md="3">
<mqtt-button width="120" height="60" class="btn green card xs12 sm6 md3 " topic="game/score/update" message='{"Green": "+1"}'>
<v-icon left size="40">mdi-plus-box</v-icon>
</mqtt-button>
</v-col>
<v-col cols="12" sm="6" md="3">
<mqtt-button width="120" height="60" class="btn green card xs12 sm6 md3 " topic="game/score/update" message='{"Green": "2"}'>
<v-icon left size="40">mdi-plus-box-multiple</v-icon>
</mqtt-button>
</v-col>
</v-row> </v-row>
</v-container> </v-container>
</v-card> </v-card>
</template> </template>
<script setup> <script setup>
import MqttButton from './MqttButton.vue'; import { ref, reactive, onMounted } from 'vue';
import { ref } from 'vue'; import mqtt from 'mqtt';
import config from '@/config.js'; // Ensure correct path
// Variable pour contrôler l'état de la carte
const isCardReduced = ref(false); const isCardReduced = ref(false);
// Méthode pour basculer l'état de la carte const scores = reactive({
Red: { Total: 0, Round: 0 },
Blue: { Total: 0, Round: 0 },
Yellow: { Total: 0, Round: 0 },
Green: { Total: 0, Round: 0 },
});
const client = mqtt.connect(config.mqttBrokerUrl);
client.on('connect', () => {
console.log('CardButtonScore: Connected to MQTT broker at', config.mqttBrokerUrl);
client.subscribe('game/score');
});
client.on('error', (err) => {
console.error('CardButtonScore: MQTT Error:', err);
});
client.on('message', (topic, message) => {
if (topic === 'game/score') {
try {
const data = JSON.parse(message.toString());
console.log('CardButtonScore: Received score update:', data);
if (data && data.TEAM) {
Object.keys(scores).forEach(color => {
if (data.TEAM[color]) {
scores[color].Total = data.TEAM[color].TotalScore;
scores[color].Round = data.TEAM[color].RoundScore;
}
});
}
} catch (e) {
console.error("Error parsing score update:", e);
}
}
});
function toggleCardSize() { function toggleCardSize() {
isCardReduced.value = !isCardReduced.value; isCardReduced.value = !isCardReduced.value;
} }
function getTeamColor(color) {
if (color === 'Yellow') return '#D4D100'; // Custom yellow
if (color === 'Red') return '#d42828';
if (color === 'Blue') return '#2867d4';
if (color === 'Green') return '#28d42e';
return color.toLowerCase();
}
function changeScore(teamColor, field, delta) {
scores[teamColor][field] += delta;
updateScore(teamColor);
}
function updateScore(teamColor) {
const payload = {
[teamColor]: {
Total: scores[teamColor].Total,
Round: scores[teamColor].Round
}
};
console.log('CardButtonScore: Publishing update:', payload);
client.publish('game/score/update', JSON.stringify(payload));
}
</script> </script>
<style> <style>
.card--reduced { .card--reduced {
height: 56px; /* Réglez la hauteur réduite selon vos besoins */ height: 56px;
width: 170px; width: 60%; /* Adjusted width for layout */
overflow: hidden; overflow: hidden;
transition: height 0.3s ease-in-out; transition: height 0.3s ease-in-out;
} }
.centered-input input {
text-align: center;
}
.centered-input .v-field__label {
text-align: center;
}
/* Remove number spin buttons */
.centered-input input[type=number]::-webkit-inner-spin-button,
.centered-input input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style> </style>

View File

@@ -53,14 +53,14 @@
<div> <div>
<v-label class="labelRoundScore-style pt-3">Manche</v-label> <v-label class="labelRoundScore-style pt-3">Manche</v-label>
<div> <div>
<v-label class="labelRoundScore-style">{{ scores.OrangeRoundScore }}</v-label> <v-label class="labelRoundScore-style">{{ scores.YellowRoundScore }}</v-label>
</div> </div>
</div> </div>
<v-divider color="background"/> <v-divider color="background"/>
<div> <div>
<v-label class="labelTotalScore-style pt-3">Total</v-label> <v-label class="labelTotalScore-style pt-3">Total</v-label>
<div> <div>
<v-label class="labelTotalScore-style pb-3">{{ scores.OrangeTotalScore }}</v-label> <v-label class="labelTotalScore-style pb-3">{{ scores.YellowTotalScore }}</v-label>
</div> </div>
</div> </div>
</v-col> </v-col>
@@ -103,11 +103,11 @@ const client = mqtt.connect(mqttBrokerUrl)
const scores = reactive({ const scores = reactive({
RedTotalScore: 0, // Propriétés réactives RedTotalScore: 0, // Propriétés réactives
BlueTotalScore: 0, // Propriétés réactives BlueTotalScore: 0, // Propriétés réactives
OrangeTotalScore: 0, // Propriétés réactives YellowTotalScore: 0, // Propriétés réactives
GreenTotalScore: 0, // Propriétés réactives GreenTotalScore: 0, // Propriétés réactives
RedRoundScore: 0, // Propriétés réactives RedRoundScore: 0, // Propriétés réactives
BlueRoundScore: 0, // Propriétés réactives BlueRoundScore: 0, // Propriétés réactives
OrangeRoundScore: 0, // Propriétés réactives YellowRoundScore: 0, // Propriétés réactives
GreenRoundScore: 0, // Propriétés réactives GreenRoundScore: 0, // Propriétés réactives
}); });
// Fonction pour traiter chaque message reçu et réinitialiser le timeout // Fonction pour traiter chaque message reçu et réinitialiser le timeout

View File

@@ -0,0 +1,47 @@
<template>
<v-container v-show="gamehiding === true" class="v-container-game-hided">
<v-img src="@\assets\v-hide.png" class="v-img-hidding"/>
</v-container>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { subscribeToTopic } from '@/services/mqttService';
let gamehiding = ref(true);
const handleMessage = (topic, message) => {
if (topic === "/display/control") {
switch (message) {
case "play":
gamehiding.value = false;
break;
case "pause":
gamehiding.value = true;
break;
default:
console.warn("Commande non reconnue :", message);
gamehiding.value = true;
}
}
};
// --- Lifecycle
onMounted(() => {
subscribeToTopic('#', (topic, message) => {
handleMessage(topic, message);
});
});
</script>
<style>
.v-img-hidding{
border-radius: 25px;
}
.v-container-game-hided{
margin-top: 40px;
width: calc(100vw - 20%) !important;
height: calc(100vh - 20%) !important;
border-radius: 25px;
}
</style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<v-container class="v-container-style-console"> <v-container class="v-container-style-console">
<v-card tile outlined width="500"> <v-card tile outlined width="900">
<v-card-title class="card__title primary centered-title"> <v-card-title class="card__title primary centered-title">
<v-icon left class="pr-5 pl-2" size="40">mdi-console-line</v-icon> <v-icon left class="pr-5 pl-2" size="40">mdi-console-line</v-icon>
Console MQTT Console MQTT
@@ -27,7 +27,7 @@
</div> </div>
</div> </div>
<v-container class="text-center"> <v-container>
<div v-for="(log, index) in filteredLogs" :key="index"> <div v-for="(log, index) in filteredLogs" :key="index">
<v-label class="v-label-timestamp">{{ log.timestamp }} -&nbsp;</v-label> <v-label class="v-label-timestamp">{{ log.timestamp }} -&nbsp;</v-label>
<v-label class="v-label-topic-message-title">Topic :&nbsp;</v-label> <v-label class="v-label-topic-message-title">Topic :&nbsp;</v-label>

View File

@@ -1,18 +1,18 @@
<template> <template>
<v-container class="v-container-style"> <v-container class="v-container-style">
<v-card tile outlined width="500"> <v-card tile outlined width="600">
<v-card-title class="card__title primary centered-title"> <v-card-title class="card__title primary centered-title">
<v-icon left class="pr-5 pl-2" size="30">mdi-send</v-icon> <v-icon left class="pr-5 pl-2" size="30">mdi-send</v-icon>
Publier un message Publier un message
</v-card-title> </v-card-title>
<div class="input-style"> <div class="input-style">
<v-select <v-text-field
label="Topic" label="Topic"
v-model="selectedTopic" v-model="selectedTopic"
:items="topics" :items="topics"
prepend-icon="mdi-target" prepend-icon="mdi-target"
></v-select> ></v-text-field>
<v-text-field <v-text-field
label="Message" label="Message"
@@ -30,6 +30,42 @@
Déblocage<br>Buzzer Déblocage<br>Buzzer
</v-btn> </v-btn>
<v-btn
rounded
color="RedBuzzer"
class="v-btn-style-standalone"
height="40"
@click="publishBuzzer('#d42828')"
>
Buzzer
</v-btn>
<v-btn
rounded
color="BlueBuzzer"
class="v-btn-style-standalone"
height="40"
@click="publishBuzzer('#2867d4')"
>
Buzzer
</v-btn>
<v-btn
rounded
color="YellowBuzzer"
class="v-btn-style-standalone"
height="40"
@click="publishBuzzer('#D4D100')"
>
Buzzer
</v-btn>
<v-btn
rounded
color="GreenBuzzer"
class="v-btn-style-standalone"
height="40"
@click="publishBuzzer('#28d42e')"
>
Buzzer
</v-btn>
<v-btn <v-btn
class="v-btn-style-validate" class="v-btn-style-validate"
height="50" height="50"
@@ -53,7 +89,8 @@ const topics = [
'display/control', 'display/control',
'sound/playsound', 'sound/playsound',
'game/score/update', 'game/score/update',
'game/score' 'game/score',
'/display/media'
]; ];
// Methods // Methods
@@ -62,7 +99,18 @@ const publisCustomMessage = () => {
}; };
const publishBuzzerUnblock = () => { const publishBuzzerUnblock = () => {
publishMessage('brainblast/buzzer/unlock', "0"); publishMessage('vulture/buzzer/unlock', "0");
};
const publishBuzzer = (inputColor) => {
publishMessage('vulture/buzzer/pressed/2',JSON.stringify({
buzzer_id: 1,
color: inputColor
}));
// Add a small delay before unlocking to ensure the score update is processed
setTimeout(() => {
publishMessage('/display/control', "pause");
}, 100);
}; };
</script> </script>

View File

@@ -0,0 +1,121 @@
<template>
<v-container v-show="gamehiding === false" class="player_video_div">
<video
ref="videoJsPlayer"
class="video-js player_video"
controls
></video>
</v-container>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
import Mysteryland_h264 from '@/quizz/Quizz-1/festival/Mysteryland_h264.mp4';
import { subscribeToTopic } from '@/services/mqttService';
let gamehiding = ref(true);
let player = ref(null);
const videoOptions = {
autoplay: false,
controls: false,
preload: 'auto',
fluid: true,
loop: false,
volume: 0,
sources: [{ src: Mysteryland_h264, type: 'video/mp4' }],
};
const handleMessage = (topic, message) => {
console.log(topic, message)
if (topic === "/display/media") {
switch (message) {
case "BOX_BEAT.mp4":
changeVideoSource("BOX_BEAT.mp4")
break;
case "DARK_VALLEY.mp4":
changeVideoSource("DARK_VALLEY.mp4")
break;
}
}
if (topic === "/display/control") {
switch (message) {
case "play":
gamehiding.value = false;
console.log("▶️ Lecture de la vidéo !");
player.value.play().catch((error) => {
console.error("Erreur de lecture :", error);
});
break;
case "pause":
gamehiding.value = true;
console.log("⏸️ Pause de la vidéo !");
player.value.pause();
break;
case "hide":
console.log("🛑 Cacher la vidéo (implémentation à venir)");
break;
}
}
};
const changeVideoSource = (relativePath) => {
try {
const fullPath = new URL(`../quizz/Quizz-1/festival/${relativePath}`, import.meta.url).href;
if (player.value) {
if (relativePath.includes("mp4")){
player.value.src({ src: fullPath, type: 'video/mp4' });
}
if (relativePath.includes(".jpg")){
player.value.src({ src: fullPath, type: 'image/jpeg' });
}
player.value.load();
player.value.play().catch((err) => console.error('Erreur lecture :', err));
}
} catch (error) {
console.error('❌ Erreur lors du chargement de la vidéo :', error);
}
};
// --- Lifecycle
onMounted(() => {
player.value = videojs(
document.querySelector('.video-js'),
videoOptions,
() => {
console.log('🎥 Video player ready');
}
);
subscribeToTopic('#', (topic, message) => {
handleMessage(topic, message);
});
});
onBeforeUnmount(() => {
if (player) {
player.dispose();
}
});
</script>
<style>
.player_video_div {
margin-top: 40px;
width: calc(100vw - 20%);
height: calc(100vh - 20%);
border-radius: 20px !important;
}
.player_video {
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
border-radius: 25px !important;
}
.vjs-tech{
border-radius: 25px;
}
</style>

View File

@@ -1,16 +1,15 @@
// Fichier vide, regarde config.js.example pour personaliser ce fichier.
// Note de dev : Normalement ce fichier ne devrait plus avoir de
// modifications
// config.js // config.js
export default { // Reads configuration from window.APP_CONFIG (loaded via public/config.js)
mqttBrokerUrl: 'ws://192.168.1.30:9001', // This allows runtime configuration changes without rebuilding the app.
// Buzzer const defaults = {
mqttBrokerUrl: 'ws://192.168.73.252:9001',
redBuzzerIP: '192.168.73.40', redBuzzerIP: '192.168.73.40',
blueBuzzerIP: '192.168.73.41', blueBuzzerIP: '192.168.73.41',
orangeBuzzerIP: '192.168.73.42', orangeBuzzerIP: '192.168.73.42',
greenBuzzerIP: '192.168.73.43' greenBuzzerIP: '192.168.73.43'
// Light
}; };
const config = window.APP_CONFIG || defaults;
export default config;

View File

@@ -5,18 +5,18 @@ export default {
RedTotalScore: 11, RedTotalScore: 11,
BlueTotalScore: 22, BlueTotalScore: 22,
GreenTotalScore: 33, GreenTotalScore: 33,
OrangeTotalScore: 44, YellowTotalScore: 44,
// Score de la manche courante // Score de la manche courante
RedRoundScore: 1, RedRoundScore: 1,
BlueRoundScore: 2, BlueRoundScore: 2,
OrangeRoundScore: 3, YellowRoundScore: 3,
GreenRoundScore: 4, GreenRoundScore: 4,
//Etat des buzzer //Etat des buzzer
BuzzerRed: false, BuzzerRed: false,
BuzzerBlue: false, BuzzerBlue: false,
BuzzerOrange: false, BuzzerYellow: false,
BuzzerGreen: false, BuzzerGreen: false,
// Ajoutez d'autres variables globales ici // Ajoutez d'autres variables globales ici
@@ -24,4 +24,4 @@ export default {
// Variables localStorage // Variables localStorage
export const localStorageVars = { // Exemple de variable localStorage RedScorelocal: localStorage.getItem('RedScore') || '', BlueScorelocal: localStorage.getItem('BlueScore') || '', OrangeScorelocal: localStorage.getItem('OrangeScore') || '', GreenScorelocal: localStorage.getItem('GreenScore') || '', export const localStorageVars = { // Exemple de variable localStorage RedScorelocal: localStorage.getItem('RedScore') || '', BlueScorelocal: localStorage.getItem('BlueScore') || '', OrangeScorelocal: localStorage.getItem('OrangeScore') || '', GreenScorelocal: localStorage.getItem('GreenScore') || '',
}; };

View File

@@ -19,6 +19,7 @@
</v-col> </v-col>
</v-row> </v-row>
</v-row> </v-row>
<BuzzerValidationDialog />
</template> </template>
<script setup> <script setup>
@@ -27,49 +28,50 @@ import CardSolution from '@/components/CardSolution.vue'
import CardControl from '@/components/CardControl.vue' import CardControl from '@/components/CardControl.vue'
import CardSoundboard from '@/components/CardSoundboard.vue'; import CardSoundboard from '@/components/CardSoundboard.vue';
import CardButtonScore from '@/components/CardButtonScore.vue' import CardButtonScore from '@/components/CardButtonScore.vue'
import BuzzerValidationDialog from '@/components/BuzzerValidationDialog.vue';
</script> </script>
<style> <style>
@media (min-width: 1024px) { @media (min-width: 1024px) {
.card__title.primary { .card__title.primary {
background-color: #d42828; /* Changez la couleur en fonction de votre thème */ background-color: rgb(var(--v-theme-primary)); /* Changez la couleur en fonction de votre thème */
} }
.card__title.feedback { .card__title.feedback {
background-color: #2E7D32; /* Changez la couleur en fonction de votre thème */ background-color: rgb(var(--v-theme-success)); /* Changez la couleur en fonction de votre thème */
} }
.btn{ .btn{
border-radius:20px!important; border-radius:20px!important;
} }
.btn.red { .btn.red {
background-color: #d42828; /* Changez la couleur en fonction de votre thème */ background-color: rgb(var(--v-theme-RedBuzzer)); /* Changez la couleur en fonction de votre thème */
} }
.btn.blue { .btn.blue {
background-color: #2867d4; /* Changez la couleur en fonction de votre thème */ background-color: rgb(var(--v-theme-BlueBuzzer)); /* Changez la couleur en fonction de votre thème */
} }
.btn.yellow { .btn.yellow {
background-color: #d4d100; /* Changez la couleur en fonction de votre thème */ background-color: rgb(var(--v-theme-YellowBuzzer)); /* Changez la couleur en fonction de votre thème */
} }
.btn.green { .btn.green {
background-color: #28d42e; /* Changez la couleur en fonction de votre thème */ background-color: rgb(var(--v-theme-GreenBuzzer)); /* Changez la couleur en fonction de votre thème */
} }
.scorediv-style-red { .scorediv-style-red {
background-color: #d42828 !important; background-color: rgb(var(--v-theme-RedBuzzer)) !important;
padding: 15px; padding: 15px;
border-top-left-radius: 10%; border-top-left-radius: 10%;
} }
.scorediv-style-yellow { .scorediv-style-yellow {
background-color: #d4d100!important; background-color: rgb(var(--v-theme-YellowBuzzer)) !important;
padding: 15px; padding: 15px;
border-bottom-left-radius: 10%; border-bottom-left-radius: 10%;
} }
.scorediv-style-blue { .scorediv-style-blue {
background-color: #2867d4 !important; background-color: rgb(var(--v-theme-BlueBuzzer)) !important;
padding: 15px; padding: 15px;
border-top-right-radius: 10%; border-top-right-radius: 10%;
} }
.scorediv-style-green { .scorediv-style-green {
background-color: #28d42e !important; background-color: rgb(var(--v-theme-GreenBuzzer)) !important;
padding: 15px; padding: 15px;
border-bottom-right-radius: 10%; border-bottom-right-radius: 10%;
} }

View File

@@ -2,113 +2,113 @@
<div class="main_div"> <div class="main_div">
<div> <div>
<v-container class="score_div_main"> <v-container class="score_div_main">
<v-container class="score_div color-blue"></v-container> <v-container class="score_div color-blue">
<v-container class="score_div color-red"></v-container> <div class="d-flex flex-column align-center">
<Transition name="score-fade" mode="out-in">
<span :key="scores.BlueRoundScore" class="v-label-round-score">Manche : {{ scores.BlueRoundScore }}</span>
</Transition>
<Transition name="score-fade" mode="out-in">
<span :key="scores.BlueTotalScore" class="v-label-score">{{ scores.BlueTotalScore }}</span>
</Transition>
</div>
</v-container>
<v-container class="score_div color-red">
<div class="d-flex flex-column align-center">
<Transition name="score-fade" mode="out-in">
<span :key="scores.RedRoundScore" class="v-label-round-score">Manche : {{ scores.RedRoundScore }}</span>
</Transition>
<Transition name="score-fade" mode="out-in">
<span :key="scores.RedTotalScore" class="v-label-score">{{ scores.RedTotalScore }}</span>
</Transition>
</div>
</v-container>
<v-container class="score_div color-white d-flex align-center justify-center"> <v-container class="score_div color-white d-flex align-center justify-center">
<span class="v-label-time">00:00</span> <span class="v-label-time">00:00</span>
</v-container> </v-container>
<v-container class="score_div color-green"></v-container> <v-container class="score_div color-green">
<v-container class="score_div color-yellow"></v-container> <div class="d-flex flex-column align-center">
<Transition name="score-fade" mode="out-in">
<span :key="scores.GreenRoundScore" class="v-label-round-score">Manche : {{ scores.GreenRoundScore }}</span>
</Transition>
<Transition name="score-fade" mode="out-in">
<span :key="scores.GreenTotalScore" class="v-label-score">{{ scores.GreenTotalScore }}</span>
</Transition>
</div>
</v-container>
<v-container class="score_div color-yellow">
<div class="d-flex flex-column align-center">
<Transition name="score-fade" mode="out-in">
<span :key="scores.YellowRoundScore" class="v-label-round-score">Manche : {{ scores.YellowRoundScore }}</span>
</Transition>
<Transition name="score-fade" mode="out-in">
<span :key="scores.YellowTotalScore" class="v-label-score">{{ scores.YellowTotalScore }}</span>
</Transition>
</div>
</v-container>
</v-container> </v-container>
</div> </div>
<div> <div>
<v-container v-show="gamehiding === true" class="v-container-game-hided"> <HidingOverlay/>
<v-img src="@\assets\v-hide.png" class="v-img-hidding"></v-img> <VideoPlayer/>
</v-container>
<v-container v-show="gamehiding === false" class="player_video_div">
<video
ref="videoJsPlayer"
class="video-js player_video"
controls
></video>
</v-container>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'; import VideoPlayer from "@/components/VideoPlayer.vue"
import videojs from 'video.js'; import HidingOverlay from "@/components/HidingOverlay.vue"
import 'video.js/dist/video-js.css'; import { onMounted, reactive } from 'vue';
import Mysteryland_h264 from '../quizz/Quizz-1/festival/Mysteryland_h264.mp4'; import mqtt from 'mqtt'
import { subscribeToTopic } from '@/services/mqttService'; import config from '@/config.js'
// --- Déclarations const mqttBrokerUrl = config.mqttBrokerUrl
const player = ref(null); const client = mqtt.connect(mqttBrokerUrl)
let gamehiding = ref(true)
const videoOptions = { const scores = reactive({
autoplay: false, RedTotalScore: 0,
controls: false, BlueTotalScore: 0,
preload: 'auto', YellowTotalScore: 0,
fluid: true, GreenTotalScore: 0,
loop: true, RedRoundScore: 0,
volume: 0, BlueRoundScore: 0,
sources: [{ src: Mysteryland_h264, type: 'video/mp4' }], YellowRoundScore: 0,
}; GreenRoundScore: 0,
// --- Fonctions
const playVideo = () => {
if (player.value) {
console.log("▶️ Lecture de la vidéo !");
player.value.play().catch((error) => {
console.error("Erreur de lecture :", error);
});
} else {
console.warn("⚠️ Player non encore initialisé !");
}
};
const pauseVideo = () => {
if (player.value) {
console.log("⏸️ Pause de la vidéo !");
player.value.pause();
} else {
console.warn("⚠️ Player non encore initialisé !");
}
};
const handleMessage = (topic, message) => {
if (topic === "/display/control") {
switch (message) {
case "play":
gamehiding.value = false;
playVideo();
break;
case "pause":
gamehiding.value = true;
pauseVideo();
break;
case "hide":
console.log("🛑 Cacher la vidéo (implémentation à venir)");
break;
default:
console.warn("Commande non reconnue :", message);
}
}
};
// --- Lifecycle
onMounted(() => {
player.value = videojs(
document.querySelector('.video-js'),
videoOptions,
() => {
console.log('🎥 Video player ready');
}
);
subscribeToTopic('#', (topic, message) => {
handleMessage(topic, message);
}); });
});
onBeforeUnmount(() => { function handleMessage(topic, message) {
if (player.value) { let parsedMessage;
player.value.dispose(); try {
parsedMessage = JSON.parse(message);
} catch (e) {
console.error("Erreur d'analyse JSON:", e);
return;
}
if (parsedMessage.TEAM) {
scores.RedTotalScore = parsedMessage.TEAM.Red.TotalScore
scores.BlueTotalScore = parsedMessage.TEAM.Blue.TotalScore
scores.YellowTotalScore = parsedMessage.TEAM.Yellow.TotalScore
scores.GreenTotalScore = parsedMessage.TEAM.Green.TotalScore
scores.RedRoundScore = parsedMessage.TEAM.Red.RoundScore
scores.BlueRoundScore = parsedMessage.TEAM.Blue.RoundScore
scores.YellowRoundScore = parsedMessage.TEAM.Yellow.RoundScore
scores.GreenRoundScore = parsedMessage.TEAM.Green.RoundScore
}
} }
});
function subscribeToTopic(topic, callback) {
client.subscribe(topic)
client.on('message', (receivedTopic, message) => { callback(receivedTopic.toString(), message.toString())
})
}
onMounted(() => {
subscribeToTopic('game/score', (topic, message) => {
handleMessage(topic, message);
});
});
</script> </script>
<style scoped> <style scoped>
@@ -122,15 +122,18 @@ onBeforeUnmount(() => {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 5px; gap: 5px;
background-color: rgb(80, 80, 80); background-color: rgb(40, 40, 40);
padding: 25px 30px; padding: 25px 30px;
border-radius: 0px 0px 30px 30px; border-radius: 0px 0px 30px 30px;
box-shadow: 0px 1px 5px rgb(255, 0, 0); box-shadow: 0px 3px 45px rgb(45, 115, 166);
} }
.score_div { .score_div {
height: 100px; height: 100px;
width: 170px; width: 170px;
text-align: center; text-align: center;
display: flex;
justify-content: center;
align-items: center;
} }
.color-blue { .color-blue {
background-color: rgb(var(--v-theme-BlueBuzzer), 1); background-color: rgb(var(--v-theme-BlueBuzzer), 1);
@@ -158,31 +161,29 @@ onBeforeUnmount(() => {
font-size: 49px; font-size: 49px;
font-family: 'Bahnschrift'; font-family: 'Bahnschrift';
} }
.player_video_div { .v-label-score {
margin-top: 40px; color: white;
width: calc(100vw - 20%); font-size: 40px;
height: calc(100vh - 20%); font-family: 'Bahnschrift';
border-radius: 20px !important; font-weight: bold;
line-height: 1;
}
.v-label-round-score {
color: rgba(255, 255, 255, 0.8);
font-size: 16px;
font-family: 'Bahnschrift';
font-weight: 500;
margin-bottom: 2px;
}
/* Transition styles */
.score-fade-enter-active,
.score-fade-leave-active {
transition: opacity 0.3s ease;
}
} .score-fade-enter-from,
.player_video { .score-fade-leave-to {
width: 100%; opacity: 0;
height: 100%;
max-width: 100vw;
max-height: 100vh;
border-radius: 25px !important;
}
.vjs-tech{
border-radius: 25px;
}
.v-container-game-hided{
margin-top: 40px;
width: calc(100vw - 20%) !important;
height: calc(100vh - 20%) !important;
border-radius: 25px;
}
.v-img-hidding{
border-radius: 25px;
} }
</style> </style>

View File

@@ -0,0 +1,60 @@
# NanoMQ Configuration 0.18.0
# #============================================================
# # NanoMQ Broker
# #============================================================
mqtt {
property_size = 32
max_packet_size = 260MB
max_mqueue_len = 2048
retry_interval = 10s
keepalive_multiplier = 1.25
# Three of below, unsupported now
max_inflight_window = 2048
max_awaiting_rel = 10s
await_rel_timeout = 10s
}
listeners.tcp {
bind = "0.0.0.0:1883"
}
listeners.ws {
bind = "0.0.0.0:9001"
}
http_server {
port = 8081
limit_conn = 2
username = admin
password = public
auth_type = basic
jwt {
public.keyfile = "/etc/certs/jwt/jwtRS256.key.pub"
}
}
log {
# to = [file, console]
to = [console]
level = warn
dir = "/tmp"
file = "nanomq.log"
rotation {
size = 10MB
count = 5
}
}
auth {
allow_anonymous = true
no_match = allow
deny_action = ignore
cache = {
max_size = 32
ttl = 1m
}
}

101
VContainers/README.md Normal file
View File

@@ -0,0 +1,101 @@
# VContainer - Vulture build script
Construction et lancements des containers.
Toutes les commandes sont à taper depuis la racine du dépôt.
## Build
```bash
./VContainers/build.sh
```
Ou manuellement :
```bash
podman build . -f ./VContainers/VNode/Containerfile -t vnode
podman build . -f ./VContainers/VApp/Containerfile -t vapp
```
## Run
### Mode Manuel avec Scripts
**Développement (localhost):**
```bash
./VContainers/run_dev.sh
```
**Production (IP 192.168.73.252):**
```bash
./VContainers/run_prod.sh
```
Les containers sont lancés sur le réseau bridge `vulture-net` :
- **nanomq** : Broker MQTT (ports 1883, 9001, 8081, 8083, 8883)
- **vnode** : Services Node.js backend
- **vapp** : Frontend Vue.js (port 8080)
## Stop
```bash
./VContainers/stop.sh
```
Ou manuellement :
```bash
podman stop vapp vnode nanomq
podman network rm vulture-net
```
## Lancement automatique avec Quadlet
Copier les fichiers du répertoire `quadlet` vers `~/.config/containers/systemd/`
```bash
cp ./VContainers/quadlet/*.network ~/.config/containers/systemd/
cp ./VContainers/quadlet/*.container ~/.config/containers/systemd/
```
**Pour l'environnement de développement :**
```bash
systemctl --user daemon-reload
systemctl --user enable --now nanomq.service
systemctl --user enable --now vnode.service
systemctl --user enable --now vapp_dev.service
```
**Pour l'environnement de production :**
```bash
systemctl --user daemon-reload
systemctl --user enable --now nanomq.service
systemctl --user enable --now vnode.service
systemctl --user enable --now vapp_prod.service
```
**Vérifier le statut :**
```bash
systemctl --user status nanomq.service vnode.service vapp_dev.service
```
**Arrêter les services :**
```bash
systemctl --user stop vapp_dev.service vnode.service nanomq.service
systemctl --user disable vapp_dev.service vnode.service nanomq.service
```
## Configuration
Les fichiers de configuration se trouvent dans `VContainers/VApp/config/` :
- `config_dev.js` : Configuration développement (MQTT sur localhost)
- `config_prod.js` : Configuration production (MQTT sur 192.168.73.252)
Vous pouvez modifier ces fichiers selon vos besoins. En mode manuel, redémarrez les containers. Avec Quadlet, redémarrez le service correspondant :
```bash
systemctl --user restart vapp_dev.service
```
## Tip
Pour permettre à Podman d'utiliser les ports privilégiés (<1024) :
```bash
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
```

View File

@@ -0,0 +1,16 @@
# Development Container for VApp
FROM docker.io/node:lts-alpine
WORKDIR /app
# Copy VApp source code
COPY VApp ./
# Install dependencies
RUN npm install
# Expose Vite default port
EXPOSE 5173
# Start in development mode with host exposure
CMD ["npm", "run", "dev", "--", "--host"]

View File

@@ -0,0 +1,11 @@
window.APP_CONFIG = {
// URL du broker MQTT (WebSockets)
// Configuration DEV : localhost
mqttBrokerUrl: 'ws://localhost:9001',
// IPs des buzzers
redBuzzerIP: '192.168.73.40',
blueBuzzerIP: '192.168.73.41',
orangeBuzzerIP: '192.168.73.42',
greenBuzzerIP: '192.168.73.43'
};

View File

@@ -0,0 +1,11 @@
window.APP_CONFIG = {
// URL du broker MQTT (WebSockets)
// Configuration PROD : IP du serveur
mqttBrokerUrl: 'ws://192.168.73.252:9001',
// IPs des buzzers
redBuzzerIP: '192.168.73.40',
blueBuzzerIP: '192.168.73.41',
orangeBuzzerIP: '192.168.73.42',
greenBuzzerIP: '192.168.73.43'
};

View File

@@ -0,0 +1,24 @@
server {
listen 80;
root /usr/share/nginx/html; # Chemin où les fichiers statiques sont copiés
index index.html index.htm; # Fichier par défaut à servir
location / {
try_files $uri $uri/ /index.html; # Pour les applications SPA (Single Page Applications)
# cela redirige toutes les requêtes non trouvées vers index.html
}
# Cache Control pour les fichiers statiques (optionnel mais bonne pratique)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
# Gzip compression (optionnel, améliore la performance)
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}

View File

@@ -0,0 +1,12 @@
FROM docker.io/keymetrics/pm2:latest-alpine
## Bundle APP files
COPY VNode src
#COPY package.json .
COPY VContainers/VNode/pm2.json .
#
## Install app dependencies
RUN cd /src && npm install
CMD [ "pm2-runtime", "start", "pm2.json" ]
#CMD [ "sh"]

View File

@@ -0,0 +1,44 @@
[
{
"name": "buzzer-manager",
"script": "services/buzzer/buzzer-manager.js",
"cwd": "/src",
"watch": false,
"env": {
"NODE_ENV": "production"
}
},
{
"name": "buzzer-watcher",
"script": "services/buzzer/buzzer-watcher.js",
"cwd": "/src",
"watch": false,
"env": {
"NODE_ENV": "production"
} },
{
"name": "quizz-collector",
"script": "services/game/quizz-collector.js",
"cwd": "/src",
"watch": false,
"env": {
"NODE_ENV": "production"
} },
{
"name": "score-manager",
"script": "services/game/score-manager.js",
"cwd": "src",
"env": {
"NODE_ENV": "production"
},
"watch": false
},
{
"name": "light-manager",
"script": "services/light/light-manager.js",
"cwd": "/src",
"watch": false,
"env": {
"NODE_ENV": "production"
} }
]

13
VContainers/build.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -e
# Move to repository root
cd "$(dirname "$0")/.."
echo "Building VNode..."
podman build . -f ./VContainers/VNode/Containerfile -t vnode
echo "Building VApp..."
podman build . -f ./VContainers/VApp/Containerfile -t vapp
echo "Build complete."

View File

@@ -0,0 +1,19 @@
[Unit]
Description=Broker MQTT NanoMQ
Wants=network-online.target
After=network-online.target
[Container]
Image=docker.io/emqx/nanomq:latest
ContainerName=nanomq
Network=vulture-net.network
PublishPort=1883:1883
PublishPort=9001:9001
PublishPort=8081:8081
PublishPort=8083:8083
PublishPort=8883:8883
Volume=%h/Src/Fablab/Vulture/VContainers/MQTT/config/nanomq.conf:/etc/nanomq.conf:Z
Exec=--conf /etc/nanomq.conf
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,12 @@
[Unit]
Description=Application Node.js VApp
Requires=vulture.pod
After=vulture.pod
[Container]
Image=localhost/vapp:latest
ContainerName=vapp
Pod=vulture
[Install]
WantedBy=vulture.pod

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Application Vue.js VApp (DEV)
Wants=network-online.target
After=network-online.target
Requires=nanomq.service
After=nanomq.service
[Container]
Image=localhost/vapp:latest
ContainerName=vapp
Network=vulture-net.network
PublishPort=8080:80
Volume=%h/Src/Fablab/Vulture/VContainers/VApp/config/config_dev.js:/usr/share/nginx/html/config.js:Z
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Application Vue.js VApp (PROD)
Wants=network-online.target
After=network-online.target
Requires=nanomq.service
After=nanomq.service
[Container]
Image=localhost/vapp:latest
ContainerName=vapp
Network=vulture-net.network
PublishPort=8080:80
Volume=%h/Src/Fablab/Vulture/VContainers/VApp/config/config_prod.js:/usr/share/nginx/html/config.js:Z
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Application Node.js VNode
Wants=network-online.target
After=network-online.target
Requires=nanomq.service
After=nanomq.service
[Container]
Image=localhost/vnode:latest
ContainerName=vnode
Network=vulture-net.network
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,6 @@
[Unit]
Description=Reseau Bridge pour Vulture
[Network]
NetworkName=vulture-net
Driver=bridge

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Pod Vulture pour le Broker MQTT et les Applications Node
Wants=network-online.target
After=network-online.target
[Pod]
# Mappings de ports : Host:Container (ces ports sont partagés par tous les conteneurs)
PublishPort=8080:80
PublishPort=1883:1883
PublishPort=8083:8083
PublishPort=8883:8883
[Install]
WantedBy=default.target

33
VContainers/run_dev.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
set -e
# Move to repository root
cd "$(dirname "$0")/.."
NETWORK_NAME="vulture-net"
echo "Creating network $NETWORK_NAME..."
if podman network exists $NETWORK_NAME; then
echo "Network $NETWORK_NAME already exists."
else
podman network create $NETWORK_NAME
fi
echo "Starting NanoMQ..."
# NanoMQ needs to expose ports for external access (e.g. VApp frontend) and be on the network for VNode
podman run -dt --rm --network $NETWORK_NAME --name nanomq \
-p 1883:1883 -p 9001:9001 -p 8081:8081 -p 8083:8083 -p 8883:8883 \
-v ./VContainers/MQTT/config/nanomq.conf:/etc/nanomq.conf:Z \
docker.io/emqx/nanomq:latest --conf /etc/nanomq.conf
echo "Starting VNode..."
# VNode connects to nanomq via the network, no ports needed on host unless for debugging
podman run -dt --rm --network $NETWORK_NAME --name vnode vnode:latest
echo "Starting VApp (DEV CONFIG)..."
# VApp (nginx) needs port 5173 exposed
podman run -dt --rm --network $NETWORK_NAME --name vapp -p 5173:5173 \
-v ./VContainers/VApp/config/config_dev.js:/usr/share/nginx/html/config.js:Z \
vapp:latest
echo "All containers started on network $NETWORK_NAME with DEV configuration."

33
VContainers/run_prod.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
set -e
# Move to repository root
cd "$(dirname "$0")/.."
NETWORK_NAME="vulture-net"
echo "Creating network $NETWORK_NAME..."
if podman network exists $NETWORK_NAME; then
echo "Network $NETWORK_NAME already exists."
else
podman network create $NETWORK_NAME
fi
echo "Starting NanoMQ..."
# NanoMQ needs to expose ports for external access (e.g. VApp frontend) and be on the network for VNode
podman run -dt --rm --network $NETWORK_NAME --name nanomq \
-p 1883:1883 -p 9001:9001 -p 8081:8081 -p 8083:8083 -p 8883:8883 \
-v ./VContainers/MQTT/config/nanomq.conf:/etc/nanomq.conf:Z \
docker.io/emqx/nanomq:latest --conf /etc/nanomq.conf
echo "Starting VNode..."
# VNode connects to nanomq via the network, no ports needed on host unless for debugging
podman run -dt --rm --network $NETWORK_NAME --name vnode vnode:latest
echo "Starting VApp (PROD CONFIG)..."
# VApp (nginx) needs port 5173 exposed
podman run -dt --rm --network $NETWORK_NAME --name vapp -p 5173:5173 \
-v ./VContainers/VApp/config/config_prod.js:/usr/share/nginx/html/config.js:Z \
vapp:latest
echo "All containers started on network $NETWORK_NAME with PROD configuration."

11
VContainers/stop.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
echo "Stopping containers..."
podman stop vapp || echo "vapp not running"
podman stop vnode || echo "vnode not running"
podman stop nanomq || echo "nanomq not running"
echo "Removing network..."
podman network rm vulture-net || echo "Network vulture-net not found"
echo "Cleanup complete."

View File

@@ -2,7 +2,7 @@
const mqtt = require('mqtt'); const mqtt = require('mqtt');
// MQTT broker configuration // MQTT broker configuration
const brokerUrl = 'mqtt://localhost'; // Broker URL (change if needed) const brokerUrl = 'mqtt://nanomq'; // Broker URL (change if needed)
const clientId = 'buzzer_manager'; const clientId = 'buzzer_manager';
const options = { const options = {
clientId, clientId,
@@ -64,8 +64,7 @@ function sendTiltStatus(action, buzzerId) {
client.publish('vulture/buzzer/status', JSON.stringify({ client.publish('vulture/buzzer/status', JSON.stringify({
status: "tilt_update", status: "tilt_update",
tilt_buzzers: tiltList, tilt_buzzers: tiltList,
message: `Buzzer ID ${buzzerId} ${action} to tilt mode`, message: `Buzzer ID ${buzzerId} ${action} to tilt mode`
timestamp: new Date().toISOString()
})); }));
console.log(`[INFO] Tilt status updated: ${tiltList.length} buzzers in tilt mode`); console.log(`[INFO] Tilt status updated: ${tiltList.length} buzzers in tilt mode`);
@@ -106,8 +105,7 @@ client.on('message', (topic, message) => {
status: "received", status: "received",
action: status, action: status,
buzzer_id: buzzer_id, buzzer_id: buzzer_id,
message: `Tilt command '${status}' received for buzzer ID ${buzzer_id}`, message: `Tilt command '${status}' received for buzzer ID ${buzzer_id}`
timestamp: new Date().toISOString()
})); }));
// Send the updated tilt status to all components // Send the updated tilt status to all components
@@ -131,8 +129,7 @@ client.on('message', (topic, message) => {
client.publish(`vulture/buzzer/confirmation/${buzzerId}`, JSON.stringify({ client.publish(`vulture/buzzer/confirmation/${buzzerId}`, JSON.stringify({
status: "received", status: "received",
buzzer_id: buzzerId, buzzer_id: buzzerId,
message: `Buzzer ID ${buzzerId} received (Color: ${color})`, message: `Buzzer ID ${buzzerId} received (Color: ${color})`
timestamp: new Date().toISOString()
})); }));
// Ignore if the buzzer is in tilt mode, but notify this event // Ignore if the buzzer is in tilt mode, but notify this event
@@ -143,8 +140,7 @@ client.on('message', (topic, message) => {
client.publish(`vulture/buzzer/tilt/ignored/${buzzerId}`, JSON.stringify({ client.publish(`vulture/buzzer/tilt/ignored/${buzzerId}`, JSON.stringify({
status: "tilt_ignored", status: "tilt_ignored",
buzzer_id: buzzerId, buzzer_id: buzzerId,
message: `Buzzer ID ${buzzerId} is in tilt mode and ignored.`, message: `Buzzer ID ${buzzerId} is in tilt`
timestamp: new Date().toISOString()
})); }));
return; return;
} }
@@ -154,8 +150,7 @@ client.on('message', (topic, message) => {
buzzer_id: buzzerId, buzzer_id: buzzerId,
color: color, color: color,
status: buzzerActive ? "blocked" : "free", status: buzzerActive ? "blocked" : "free",
message: `Activity detected on buzzer ID ${buzzerId} (Color: ${color})`, message: `Activity detected on buzzer ID ${buzzerId} (Color: ${color})`
timestamp: new Date().toISOString()
})); }));
if (!buzzerActive) { if (!buzzerActive) {
@@ -176,8 +171,7 @@ client.on('message', (topic, message) => {
status: "blocked", status: "blocked",
buzzer_id: buzzerId, buzzer_id: buzzerId,
color: color, color: color,
message: `Buzzer activated by ID ${buzzerId} (Color: ${color})`, message: `Buzzer activated by ID ${buzzerId} (Color: ${color})`
timestamp: new Date().toISOString()
})); }));
console.log(`[INFO] Buzzers blocked and notification sent`); console.log(`[INFO] Buzzers blocked and notification sent`);
@@ -188,19 +182,12 @@ client.on('message', (topic, message) => {
if (topic === 'vulture/buzzer/unlock') { if (topic === 'vulture/buzzer/unlock') {
console.log('[INFO] Buzzer unlock requested'); console.log('[INFO] Buzzer unlock requested');
// Confirm receipt of unlock command
client.publish('vulture/buzzer/unlock/confirmation', JSON.stringify({
status: "received",
message: "Buzzer unlock command received.",
timestamp: new Date().toISOString()
}));
// Notify the light manager to change to the team's color // Notify the light manager to change to the team's color
client.publish('vulture/light/change', JSON.stringify({ client.publish('vulture/light/change', JSON.stringify({
color: "#FFFFFF", color: "#FFFFFF",
effect: 'rainbow' effect: 'rainbow'
})); }));
// Reset buzzer manager state // Reset buzzer manager state
buzzerActive = false; buzzerActive = false;
buzzerThatPressed = null; buzzerThatPressed = null;
@@ -208,8 +195,7 @@ client.on('message', (topic, message) => {
// Notify all components of buzzer unlock // Notify all components of buzzer unlock
client.publish('vulture/buzzer/status', JSON.stringify({ client.publish('vulture/buzzer/status', JSON.stringify({
status: "unblocked", status: "unblocked",
message: "Buzzers unblocked and ready for activation.", message: "Buzzers unblocked and ready for activation."
timestamp: new Date().toISOString()
})); }));
console.log('[INFO] Buzzers unblocked and notification sent'); console.log('[INFO] Buzzers unblocked and notification sent');

View File

@@ -1,9 +1,10 @@
const path = require('path');
const ping = require('ping'); const ping = require('ping');
const mqtt = require('mqtt'); const mqtt = require('mqtt');
const fs = require('fs'); const fs = require('fs');
// Lecture du fichier de configuration // Lecture du fichier de configuration
const config = JSON.parse(fs.readFileSync('\services\\config\\config_network.json', 'utf8')); const config = JSON.parse(fs.readFileSync(path.join('services','config','config_network.json'), 'utf8'));
// Extraction des informations de config // Extraction des informations de config
const { hosts: { buzzers: { IP: buzzerIPs, MQTTconfig: { mqttHost, mqttTopic } } } } = config; const { hosts: { buzzers: { IP: buzzerIPs, MQTTconfig: { mqttHost, mqttTopic } } } } = config;

View File

@@ -1,18 +1,17 @@
{ {
"services": { "services": {
"mqttHost": "mqtt://192.168.1.30", "mqttHost": "mqtt://nanomq",
"score":{ "score": {
"MQTTconfig":{ "MQTTconfig": {
"mqttScoreTopic": "game/score", "mqttScoreTopic": "game/score",
"mqttScoreChangeTopic": "game/score/update" "mqttScoreChangeTopic": "game/score/update"
} }
}, },
"quizzcollector":{ "quizzcollector": {
"MQTTconfig":{ "MQTTconfig": {
"mqttQuizzCollectorListTopic": "game/quizz-collector/list", "mqttQuizzCollectorListTopic": "game/quizz-collector/list",
"mqttQuizzCollectorCmdTopic": "game/quizz-collector/cmd" "mqttQuizzCollectorCmdTopic": "game/quizz-collector/cmd"
} }
} }
} }
} }

View File

@@ -1,17 +1,16 @@
{ {
"hosts": { "hosts": {
"buzzers":{ "buzzers": {
"IP":{ "IP": {
"redBuzzerIP": "8.8.8.6", "redBuzzerIP": "192.168.73.40",
"blueBuzzerIP": "8.8.8.8", "blueBuzzerIP": "192.168.73.41",
"greenBuzzerIP": "8.8.8.8", "greenBuzzerIP": "192.168.73.42",
"yellowBuzzerIP": "8.8.8.8" "yellowBuzzerIP": "192.168.73.43"
}, },
"MQTTconfig":{ "MQTTconfig": {
"mqttHost": "mqtt://192.168.1.28", "mqttHost": "mqtt://nanomq",
"mqttTopic": "buzzer/watcher" "mqttTopic": "buzzer/watcher"
} }
} }
} }
} }

View File

@@ -1,8 +1,9 @@
const fs = require('fs'); const fs = require('fs');
const mqtt = require('mqtt'); const mqtt = require('mqtt');
const path = require('path');
// Lecture du fichier de configuration // Lecture du fichier de configuration
const config = JSON.parse(fs.readFileSync('\services\\config\\config_game.json', 'utf8')); const config = JSON.parse(fs.readFileSync(path.join('services','config','config_game.json'), 'utf8'));
// Extraction des informations de config // Extraction des informations de config
const { services: { mqttHost, quizzcollector: { MQTTconfig: { mqttQuizzCollectorListTopic, mqttQuizzCollectorCmdTopic } } } } = config; const { services: { mqttHost, quizzcollector: { MQTTconfig: { mqttQuizzCollectorListTopic, mqttQuizzCollectorCmdTopic } } } } = config;

View File

@@ -1,9 +1,9 @@
const args = process.argv; const args = process.argv;
// Vérification si un paramètre est passé, si c'est le cas c'est qu'on fournis un fichier de score pour reprendre le jeu la ou il était // Vérification si un paramètre est passé, si c'est le cas c'est qu'on fournis un fichier de score pour reprendre le jeu la ou il était
if (args[2] !== undefined){ if (args[2] !== undefined) {
global.ScoreFile = args[2] global.ScoreFile = args[2]
}else{ } else {
global.ScoreFile = "XXX.json" global.ScoreFile = "XXX.json"
} }
@@ -50,36 +50,36 @@ fs.access(filePath, fs.constants.F_OK, (err) => {
const initialContent = { const initialContent = {
"TEAM": { "TEAM": {
"Red": { "Red": {
"Name": "XXX", "Name": "XXX",
"TotalScore": 0, "TotalScore": 0,
"RoundScore": 0, "RoundScore": 0,
"Penality": 0, "Penality": 0,
"MasterPoint": 0 "MasterPoint": 0
}, },
"Blue": { "Blue": {
"Name": "XXX", "Name": "XXX",
"TotalScore": 0, "TotalScore": 0,
"RoundScore": 0, "RoundScore": 0,
"Penality": 0, "Penality": 0,
"MasterPoint": 0 "MasterPoint": 0
}, },
"Yellow": { "Yellow": {
"Name": "XXX", "Name": "XXX",
"TotalScore": 0, "TotalScore": 0,
"RoundScore": 0, "RoundScore": 0,
"Penality": 0, "Penality": 0,
"MasterPoint": 0 "MasterPoint": 0
}, },
"Green": { "Green": {
"Name": "XXX", "Name": "XXX",
"TotalScore": 0, "TotalScore": 0,
"RoundScore": 0, "RoundScore": 0,
"Penality": 0, "Penality": 0,
"MasterPoint": 0 "MasterPoint": 0
} }
} }
}; };
fs.writeFile(newFilePath, JSON.stringify(initialContent, null, 2), (err) => { fs.writeFile(newFilePath, JSON.stringify(initialContent, null, 2), (err) => {
if (err) { if (err) {
console.error("Erreur de création du fichier :", err); console.error("Erreur de création du fichier :", err);
@@ -108,10 +108,10 @@ function updateTeamTotalScore(teamColor, points) {
if (!jsonData.TEAM.hasOwnProperty(teamColor)) { if (!jsonData.TEAM.hasOwnProperty(teamColor)) {
console.error(`L'équipe ${teamColor} n'existe pas.`); console.error(`L'équipe ${teamColor} n'existe pas.`);
return; return;
} }
const change = parseInt(points, 10); const change = parseInt(points, 10);
// Mettre à jour le score // Mettre à jour le score
jsonData.TEAM[teamColor].TotalScore += points; jsonData.TEAM[teamColor].TotalScore += points;
console.log(`Le score total pour l'équipe ${teamColor} est de ${jsonData.TEAM[teamColor].TotalScore} points !`) console.log(`Le score total pour l'équipe ${teamColor} est de ${jsonData.TEAM[teamColor].TotalScore} points !`)
@@ -132,11 +132,15 @@ function updateTeamTotalScore(teamColor, points) {
// Lecture du fichier de configuration // Lecture du fichier de configuration
const config = JSON.parse(fs.readFileSync('\services\\config\\config_game.json', 'utf8')); const configPath = path.join(__dirname, '../config/config_game.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
// Extraction des informations de config // Extraction des informations de config
const { services: { mqttHost, score: { MQTTconfig: { mqttScoreTopic, mqttScoreChangeTopic } } } } = config; const { services: { mqttHost, score: { MQTTconfig: { mqttScoreTopic, mqttScoreChangeTopic } } } } = config;
console.log(mqttScoreChangeTopic) console.log("DEBUG: Config loaded from:", configPath);
console.log("DEBUG: MQTT Host:", mqttHost);
console.log("DEBUG: Topics:", mqttScoreTopic, mqttScoreChangeTopic);
// Connexion au broker MQTT // Connexion au broker MQTT
const client = mqtt.connect(mqttHost); const client = mqtt.connect(mqttHost);
@@ -156,6 +160,8 @@ client.on('message', (topic, message) => {
let process; let process;
let Team; let Team;
let Action; let Action;
let TotalScore = null;
let RoundScore = null;
try { try {
// Analyse du message reçu // Analyse du message reçu
@@ -168,63 +174,82 @@ client.on('message', (topic, message) => {
if (payload && typeof payload === 'object') { if (payload && typeof payload === 'object') {
// Extraire la clé (la couleur) et la valeur associée // Extraire la clé (la couleur) et la valeur associée
Team = Object.keys(payload)[0]; // La première (et unique) clé Team = Object.keys(payload)[0]; // La première (et unique) clé
Action = payload[Team]; // La valeur associée let value = payload[Team]; // La valeur associée
//console.log(`Team: ${Team}, Action: ${Action}`);
if (typeof value === 'object') {
// Mode SET (valeur absolue)
if (value.hasOwnProperty('Total')) TotalScore = parseInt(value.Total, 10);
if (value.hasOwnProperty('Round')) RoundScore = parseInt(value.Round, 10);
Action = "SET";
} else {
// Mode ADD (relatif)
Action = value;
}
process = true; process = true;
} else { } else {
console.error(typeof payload); console.error(typeof payload);
process = false; process = false;
} }
if (process === true){ if (process === true) {
let currentScore = 0; if (Action === "SET") {
let change = 0 ; // Mise à jour absolue
switch (Team){ updateTeamScoreAbsolute(Team, TotalScore, RoundScore);
case "Red": } else {
change = parseInt(Action, 10); // Convertit 'action' en entier // Mise à jour relative (existant)
if (!isNaN(change)) { let change = parseInt(Action, 10);
updateTeamTotalScore("Red", change) if (!isNaN(change)) {
} else { updateTeamTotalScore(Team, change);
console.error(`Action invalide : ${action}`); } else {
} console.error(`Action invalide : ${Action}`);
break; }
case "Blue":
change = parseInt(Action, 10); // Convertit 'action' en entier
if (!isNaN(change)) {
updateTeamTotalScore("Blue", change)
} else {
console.error(`Action invalide : ${action}`);
}
break;
case "Green":
change = parseInt(Action, 10); // Convertit 'action' en entier
if (!isNaN(change)) {
updateTeamTotalScore("Green", change)
} else {
console.error(`Action invalide : ${action}`);
}
break;
case "Yellow":
change = parseInt(Action, 10); // Convertit 'action' en entier
if (!isNaN(change)) {
updateTeamTotalScore("Yellow", change)
} else {
console.error(`Action invalide : ${action}`);
}
break;
} }
} }
}); });
// Fonction pour mettre à jour le score d'une équipe (Absolu)
function updateTeamScoreAbsolute(teamColor, totalScore, roundScore) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error("Erreur de lecture du fichier :", err);
return;
}
try {
const jsonData = JSON.parse(data);
if (!jsonData.TEAM.hasOwnProperty(teamColor)) {
console.error(`L'équipe ${teamColor} n'existe pas.`);
return;
}
if (totalScore !== null && !isNaN(totalScore)) {
jsonData.TEAM[teamColor].TotalScore = totalScore;
}
if (roundScore !== null && !isNaN(roundScore)) {
jsonData.TEAM[teamColor].RoundScore = roundScore;
}
console.log(`Mise à jour absolue pour ${teamColor} -> Total: ${jsonData.TEAM[teamColor].TotalScore}, Round: ${jsonData.TEAM[teamColor].RoundScore}`);
client.publish(mqttScoreTopic, JSON.stringify(jsonData));
fs.writeFile(filePath, JSON.stringify(jsonData, null, 2), (err) => {
if (err) console.error("Erreur d'écriture :", err);
});
} catch (parseErr) {
console.error("Erreur JSON :", parseErr);
}
});
}
(async () => { (async () => {
while (true) { while (true) {
console.log("Boucle en arrière-plan"); console.log("Boucle en arrière-plan");
await new Promise((resolve) => setTimeout(resolve, 2000)); // Pause de 2 secondes await new Promise((resolve) => setTimeout(resolve, 2000)); // Pause de 2 secondes
//client.publish(mqttScoreTopic, JSON.stringify(global.jsonData)); //client.publish(mqttScoreTopic, JSON.stringify(global.jsonData));
} }
})(); })();
client.on('error', (error) => { client.on('error', (error) => {
console.error('Erreur de connexion au broker MQTT:', error.message); console.error('Erreur de connexion au broker MQTT:', error.message);

View File

@@ -2,7 +2,7 @@
const mqtt = require('mqtt'); const mqtt = require('mqtt');
// Configuration du broker MQTT et de WLED // Configuration du broker MQTT et de WLED
const brokerUrl = 'mqtt://localhost'; // Change ce lien si nécessaire const brokerUrl = 'mqtt://nanomq'; // Change ce lien si nécessaire
const clientId = 'light_manager_wled'; const clientId = 'light_manager_wled';
const wledTopicBase = 'wled/all'; // Le topic de base pour ton ruban WLED const wledTopicBase = 'wled/all'; // Le topic de base pour ton ruban WLED
const options = { const options = {