Tracking de l'application VApp (IHM du jeu)
30
VApp/src/App.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<BrainBlastBar v-if="$route.name != 'Game Display (Projection)'" />
|
||||
<GameStatus v-if="$route.name === 'Game Control (Présentateur)'">
|
||||
</GameStatus>
|
||||
<v-main>
|
||||
<RouterView />
|
||||
</v-main> <!-- <v-footer class="footer" :elevation=12 border><v-row justify="center">© 2024 - ASCO section Fablab</v-row></v-footer> -->
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import BrainBlastBar from '@/components/BrainBlastBar.vue'
|
||||
import GameStatus from '@/components/GameStatus.vue'
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html {
|
||||
overflow: auto !important;
|
||||
}
|
||||
.footer {
|
||||
position: fixed; /* Fixe le footer en bas de la page */
|
||||
bottom: 0; /* Aligne le footer en bas de la page */
|
||||
left: 0; /* Aligne le footer à gauche */
|
||||
width: 100%; /* Ajuste la largeur du footer en fonction de la largeur de l'écran */
|
||||
z-index: 1000; /* Assure que le footer est au-dessus des autres éléments */
|
||||
}
|
||||
</style>
|
||||
BIN
VApp/src/assets/BrainBlast-Ai-Upscaled.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
VApp/src/assets/BrainBlast-For-HomeView-Alpha.png
Normal file
|
After Width: | Height: | Size: 11 MiB |
BIN
VApp/src/assets/BrainBlast-For-HomeView.jpg
Normal file
|
After Width: | Height: | Size: 768 KiB |
BIN
VApp/src/assets/V-hide.png
Normal file
|
After Width: | Height: | Size: 766 KiB |
BIN
VApp/src/assets/copilot-solution-.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
VApp/src/assets/copilot-solution-FULL-HD.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
VApp/src/assets/copilot-solution-UPSCALE.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
VApp/src/assets/design.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
10
VApp/src/components/BrainBlastBar.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<v-app-bar :collapse="$route.name === 'Game Display (Projection)'" :elevation="5" height="50">
|
||||
<RouterMenu />
|
||||
<v-app-bar-title v-if="$route.name !== 'Accueil'">Brain Blast</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RouterMenu from '@/components/RouterMenu.vue'
|
||||
</script>
|
||||
120
VApp/src/components/BuzzerWatcherCard.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="label-pos">
|
||||
<v-label class="labelTitle-style">Buzzer connectés</v-label>
|
||||
</div>
|
||||
<v-row no-gutters justify="space-around" class="button-pos">
|
||||
<v-icon v-bind:color="redBuzzerState == 1 ? 'RedBuzzer' : 'DisconnectedBuzzer'">mdi-radiobox-marked</v-icon>
|
||||
<v-icon v-bind:color="blueBuzzerState == 1 ? 'BlueBuzzer' : 'DisconnectedBuzzer'">mdi-radiobox-marked</v-icon>
|
||||
<v-icon v-bind:color="yellowBuzzerState == 1 ? 'YellowBuzzer' : 'DisconnectedBuzzer'">mdi-radiobox-marked</v-icon>
|
||||
<v-icon v-bind:color="greenBuzzerState == 1 ? 'GreenBuzzer' : 'DisconnectedBuzzer'">mdi-radiobox-marked</v-icon>
|
||||
</v-row>
|
||||
<v-divider :thickness="2" class="border-opacity-100" color="primary"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { subscribeToTopic } from '@/services/mqttService'
|
||||
import { onMounted, ref, onUnmounted } from 'vue';
|
||||
|
||||
// États réactifs pour chaque buzzer
|
||||
let redBuzzerState = ref(0);
|
||||
let blueBuzzerState = ref(0);
|
||||
let greenBuzzerState = ref(0);
|
||||
let yellowBuzzerState = ref(0);
|
||||
|
||||
// État pour surveiller la connexion générale
|
||||
let connectionStatus = ref("connected"); // "connected" ou "disconnected"
|
||||
|
||||
// Variable pour gérer le timeout global
|
||||
let globalTimeoutHandle = null;
|
||||
|
||||
// Fonction pour réinitialiser le timeout global
|
||||
function resetGlobalTimeout() {
|
||||
// Effacer le timeout précédent, s'il existe
|
||||
if (globalTimeoutHandle) {
|
||||
clearTimeout(globalTimeoutHandle);
|
||||
}
|
||||
|
||||
// Redémarrer un timeout de 5 minutes (300000 ms)
|
||||
globalTimeoutHandle = setTimeout(() => {
|
||||
handleGlobalTimeout(); // Appel si aucun message MQTT reçu depuis 5 minutes
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
// Fonction à exécuter si le timeout global est atteint
|
||||
function handleGlobalTimeout() {
|
||||
console.log("Aucun message MQTT reçu depuis plus de 5 minutes !");
|
||||
connectionStatus.value = "disconnected"; // Indiquer que la connexion est perdue
|
||||
redBuzzerState.value = 0;
|
||||
blueBuzzerState.value = 0;
|
||||
yellowBuzzerState.value = 0;
|
||||
greenBuzzerState.value = 0;
|
||||
|
||||
// Tu peux ajouter ici d'autres actions, comme afficher une alerte
|
||||
}
|
||||
|
||||
// Fonction pour traiter chaque message reçu et réinitialiser le timeout
|
||||
function handleMessage(topic, message) {
|
||||
let parsedMessage;
|
||||
try {
|
||||
parsedMessage = JSON.parse(message);
|
||||
} catch (e) {
|
||||
console.error("Erreur d'analyse JSON:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extraire les informations
|
||||
const { buzzer, status } = parsedMessage;
|
||||
|
||||
// Mettre à jour l'état des buzzers en fonction des messages
|
||||
switch (buzzer) {
|
||||
case 'redBuzzerIP':
|
||||
redBuzzerState.value = status === "online" ? 1 : 0;
|
||||
break;
|
||||
case 'blueBuzzerIP':
|
||||
blueBuzzerState.value = status === "online" ? 1 : 0;
|
||||
break;
|
||||
case 'yellowBuzzerIP':
|
||||
yellowBuzzerState.value = status === "online" ? 1 : 0;
|
||||
break;
|
||||
case 'greenBuzzerIP':
|
||||
greenBuzzerState.value = status === "online" ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Réinitialiser le timeout global car un message a été reçu
|
||||
connectionStatus.value = "connected"; // Rétablir le statut de connexion
|
||||
resetGlobalTimeout();
|
||||
}
|
||||
|
||||
// S'abonner au topic lorsque le composant est monté
|
||||
onMounted(() => {
|
||||
subscribeToTopic('buzzer/watcher', (topic, message) => {
|
||||
handleMessage(topic, message);
|
||||
resetGlobalTimeout(); // Réinitialiser le timeout global au démarrage
|
||||
});
|
||||
});
|
||||
|
||||
// Nettoyer le timeout global lorsque le composant est démonté
|
||||
onUnmounted(() => {
|
||||
if (globalTimeoutHandle) {
|
||||
clearTimeout(globalTimeoutHandle);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.label-pos {
|
||||
padding-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.labelTitle-style {
|
||||
font-size: 20px !important;
|
||||
font-weight: 500;
|
||||
color: #e91e1e !important;
|
||||
opacity: 90% !important;
|
||||
}
|
||||
.button-pos {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
114
VApp/src/components/CardButtonScore.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<v-card tile outlined :class="{ 'card--reduced': isCardReduced }">
|
||||
<v-card-title class="card__title primary" @click="toggleCardSize">
|
||||
<v-icon left class="white--text pr-5 pl-2" size="40">mdi-calculator-variant</v-icon>
|
||||
Gestion des scores
|
||||
</v-card-title>
|
||||
<v-container class="text-center">
|
||||
<v-row justify="center">
|
||||
<v-col cols="4" sm="6" md="3">
|
||||
<mqtt-button width="120" height="60" class="btn red xs12 sm6 md3" topic="game/score/update" message='{"Red": "-2"}'>
|
||||
<v-icon left size="40">mdi-minus-box-multiple</v-icon>
|
||||
</mqtt-button>
|
||||
</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"}'>
|
||||
<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 red card xs12 sm6 md3" topic="game/score/update" message='{"Red": "+1"}'>
|
||||
<v-icon left size="40">mdi-plus-box</v-icon>
|
||||
</mqtt-button>
|
||||
</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": "+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 blue card xs12 sm6 md3 " topic="game/score/update" message='{"Blue": "-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 blue card xs12 sm6 md3 " topic="game/score/update" message='{"Blue": "-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 blue card xs12 sm6 md3 " topic="game/score/update" message='{"Blue": "+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 blue card xs12 sm6 md3 " topic="game/score/update" message='{"Blue": "+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 yellow card xs12 sm6 md3 " topic="game/score/update" message='{"Yellow": "-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 yellow card xs12 sm6 md3 " topic="game/score/update" message='{"Yellow": "-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 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-container>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MqttButton from './MqttButton.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
// Variable pour contrôler l'état de la carte
|
||||
const isCardReduced = ref(false);
|
||||
|
||||
// Méthode pour basculer l'état de la carte
|
||||
function toggleCardSize() {
|
||||
isCardReduced.value = !isCardReduced.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card--reduced {
|
||||
height: 56px; /* Réglez la hauteur réduite selon vos besoins */
|
||||
width: 170px;
|
||||
overflow: hidden;
|
||||
transition: height 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
53
VApp/src/components/CardControl.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<v-card tile outlined :class="{ 'card--reduced': isCardReduced }">
|
||||
<v-card-title class="card__title primary" @click="toggleCardSize">
|
||||
<v-icon left class="white--text pr-5 pl-2" size="40">mdi-camera-control</v-icon>
|
||||
Contrôle du jeu
|
||||
</v-card-title>
|
||||
<v-container class="text-center">
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="6" md="5" class="mt-4">
|
||||
<mqtt-button width="150" height="90" class="btn red" topic="/display/control" message="previous">
|
||||
<v-icon left size="60">mdi-skip-previous</v-icon>
|
||||
</mqtt-button>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="5" class="mt-4">
|
||||
<mqtt-button width="150" height="90" class="btn red card" topic="/display/control" message="next">
|
||||
<v-icon left size="60">mdi-skip-next</v-icon>
|
||||
</mqtt-button>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="5" class="mb-4">
|
||||
<mqtt-button width="150" height="90" class="btn red card" topic="/display/control" message="pause">
|
||||
<v-icon left size="60">mdi-pause</v-icon>
|
||||
</mqtt-button>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="5" class="mb-4">
|
||||
<mqtt-button width="150" height="90" class="btn red card" topic="/display/control" message="play">
|
||||
<v-icon left size="60">mdi-play</v-icon>
|
||||
</mqtt-button>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MqttButton from './MqttButton.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
// Variable pour contrôler l'état de la carte
|
||||
const isCardReduced = ref(false);
|
||||
|
||||
// Méthode pour basculer l'état de la carte
|
||||
function toggleCardSize() { isCardReduced.value = !isCardReduced.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card--reduced {
|
||||
height: 56px; /* Réglez la hauteur réduite selon vos besoins */
|
||||
width: 170px;
|
||||
overflow: hidden;
|
||||
transition: height 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
64
VApp/src/components/CardCurrentQuizz.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="label-pos">
|
||||
<v-label class="labelTitle-style pb-4">Nom du Quizz</v-label>
|
||||
</div>
|
||||
|
||||
<!-- Sélection + Bouton -->
|
||||
<div class="select-style-div">
|
||||
<v-select
|
||||
label="Select"
|
||||
:items="quizzList"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="v-select-color"
|
||||
/>
|
||||
<v-btn color="primary" @click="publisCollectMessage" text="Mettre à jour"></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { subscribeToTopic, publishMessage } from '@/services/mqttService';
|
||||
|
||||
const mqttQuizzCollectorList = 'game/quizz-collector/list';
|
||||
const mqttQuizzCollectorCmd = 'game/quizz-collector/cmd';
|
||||
|
||||
const quizzList = ref([]);
|
||||
|
||||
// Fonction pour mettre à jour la liste
|
||||
const handleMessage = (topic, message) => {
|
||||
try {
|
||||
quizzList.value = JSON.parse(message.toString());
|
||||
} catch (error) {
|
||||
console.error('Erreur de parsing JSON:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fonction pour republier la commande "Collect"
|
||||
const publisCollectMessage = () => {
|
||||
publishMessage(mqttQuizzCollectorCmd, 'Collect');
|
||||
};
|
||||
|
||||
// Au montage du composant
|
||||
onMounted(() => {
|
||||
subscribeToTopic(mqttQuizzCollectorList, (topic, message) => {
|
||||
handleMessage(topic, message);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
publisCollectMessage();
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.select-style-div {
|
||||
padding: 2% 10% 2% 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.v-select-color {
|
||||
color: rgba(var(--v-theme-primary), 1.0);
|
||||
opacity: 100%;
|
||||
}
|
||||
</style>
|
||||
218
VApp/src/components/CardScore.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div class="label-pos">
|
||||
<v-label class="labelTitle-style pb-4">Scores</v-label>
|
||||
</div>
|
||||
<!-- Équipes Rouges et Bleues côte à côte -->
|
||||
<v-row no-gutters class="scorebox-pos"> <!-- Équipe Rouge -->
|
||||
<v-col cols="6"> <!-- Colonnes de taille 6 pour chaque équipe -->
|
||||
<v-row no-gutters>
|
||||
<v-col class="scorediv-style-red">
|
||||
<div>
|
||||
<v-label class="labelRoundScore-style pt-3">Manche</v-label>
|
||||
<div>
|
||||
<v-label class="labelRoundScore-style">{{ scores.RedRoundScore }}</v-label>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider color="background"/>
|
||||
<div>
|
||||
<v-label class="labelTotalScore-style pt-3">Total</v-label>
|
||||
<div>
|
||||
<v-label class="labelTotalScore-style pb-3">{{ scores.RedTotalScore }}</v-label>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<!-- Équipe Bleue -->
|
||||
<v-col cols="6">
|
||||
<v-row no-gutters>
|
||||
<v-col class="scorediv-style-blue">
|
||||
<div>
|
||||
<v-label class="labelRoundScore-style pt-3">Manche</v-label>
|
||||
<div>
|
||||
<v-label class="labelRoundScore-style">{{ scores.BlueRoundScore }}</v-label>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider color="background"/>
|
||||
<div>
|
||||
<v-label class="labelTotalScore-style pt-3">Total</v-label>
|
||||
<div>
|
||||
<v-label class="labelTotalScore-style pb-3">{{ scores.BlueTotalScore }}</v-label>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- Équipes Oranges et Vertes côte à côte -->
|
||||
<v-row no-gutters class="scorebox-pos">
|
||||
<!-- Équipe Orange -->
|
||||
<v-col cols="6">
|
||||
<v-row no-gutters>
|
||||
<v-col class="scorediv-style-yellow">
|
||||
<div>
|
||||
<v-label class="labelRoundScore-style pt-3">Manche</v-label>
|
||||
<div>
|
||||
<v-label class="labelRoundScore-style">{{ scores.OrangeRoundScore }}</v-label>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider color="background"/>
|
||||
<div>
|
||||
<v-label class="labelTotalScore-style pt-3">Total</v-label>
|
||||
<div>
|
||||
<v-label class="labelTotalScore-style pb-3">{{ scores.OrangeTotalScore }}</v-label>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<!-- Équipe Verte -->
|
||||
<v-col cols="6">
|
||||
<v-row no-gutters>
|
||||
<v-col class="scorediv-style-green">
|
||||
<div>
|
||||
<v-label class="labelRoundScore-style pt-3">Manche</v-label>
|
||||
<div>
|
||||
<v-label class="labelRoundScore-style">{{ scores.GreenRoundScore }}</v-label>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider color="background"/>
|
||||
<div>
|
||||
<v-label class="labelTotalScore-style pt-3">Total</v-label>
|
||||
<div>
|
||||
<v-label class="labelTotalScore-style pb-3">{{ scores.GreenTotalScore }}</v-label>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, reactive } from 'vue';
|
||||
import variables from '@/variables.js';
|
||||
import mqtt from 'mqtt'
|
||||
import config from '@/config.js'
|
||||
|
||||
const mqttBrokerUrl = config.mqttBrokerUrl
|
||||
// Créer une instance de client MQTT
|
||||
const client = mqtt.connect(mqttBrokerUrl)
|
||||
|
||||
// Déclaration des variables locales pour les scores
|
||||
const scores = reactive({
|
||||
RedTotalScore: 0, // Propriétés réactives
|
||||
BlueTotalScore: 0, // Propriétés réactives
|
||||
OrangeTotalScore: 0, // Propriétés réactives
|
||||
GreenTotalScore: 0, // Propriétés réactives
|
||||
RedRoundScore: 0, // Propriétés réactives
|
||||
BlueRoundScore: 0, // Propriétés réactives
|
||||
OrangeRoundScore: 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
|
||||
function handleMessage(topic, message) {
|
||||
let parsedMessage;
|
||||
try {
|
||||
parsedMessage = JSON.parse(message);
|
||||
} catch (e) {
|
||||
console.error("Erreur d'analyse JSON:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extraire les informations
|
||||
//const { TEAM, Name } = parsedMessage;
|
||||
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
|
||||
// Mettre à jour l'état des buzzers en fonction des messages
|
||||
/*
|
||||
switch (buzzer) {
|
||||
case 'redBuzzerIP':
|
||||
redBuzzerState.value = status === "online" ? 1 : 0;
|
||||
break;
|
||||
case 'blueBuzzerIP':
|
||||
blueBuzzerState.value = status === "online" ? 1 : 0;
|
||||
break;
|
||||
case 'yellowBuzzerIP':
|
||||
yellowBuzzerState.value = status === "online" ? 1 : 0;
|
||||
break;
|
||||
case 'greenBuzzerIP':
|
||||
greenBuzzerState.value = status === "online" ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
function subscribeToTopic(topic, callback) {
|
||||
client.subscribe(topic)
|
||||
client.on('message', (receivedTopic, message) => { callback(receivedTopic.toString(), message.toString())
|
||||
})
|
||||
}
|
||||
// S'abonner au topic lorsque le composant est monté
|
||||
onMounted(() => {
|
||||
subscribeToTopic('game/score', (topic, message) => {
|
||||
handleMessage(topic, message);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.label-pos {
|
||||
padding-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.labelTitle-style {
|
||||
font-size: 20px !important;
|
||||
font-weight: 500;
|
||||
color: #d42828 !important;
|
||||
opacity: 90% !important;
|
||||
}
|
||||
.labelRoundScore-style {
|
||||
opacity: 100% !important;
|
||||
font-size: 25px !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.labelTotalScore-style {
|
||||
opacity: 100% !important;
|
||||
font-size: 15px !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.button-pos {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.scorebox-pos {
|
||||
text-align: center;
|
||||
}
|
||||
.scorediv-style-red {
|
||||
background-color: #d42828 !important;
|
||||
padding: 15px;
|
||||
border-top-left-radius: 10%;
|
||||
}
|
||||
.scorediv-style-yellow {
|
||||
background-color: #d4d100 !important;
|
||||
padding: 15px;
|
||||
border-bottom-left-radius: 10%;
|
||||
}
|
||||
.scorediv-style-blue {
|
||||
background-color: #2867d4 !important;
|
||||
padding: 15px;
|
||||
border-top-right-radius: 10%;
|
||||
}
|
||||
.scorediv-style-green {
|
||||
background-color: #28d42e !important;
|
||||
padding: 15px;
|
||||
border-bottom-right-radius: 10%;
|
||||
}
|
||||
</style>
|
||||
46
VApp/src/components/CardSolution.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<v-card tile outlined :class="{ 'card--reduced': isCardReduced }">
|
||||
<v-card-title class="card__title primary" @click="toggleCardSize">
|
||||
<v-icon left class="white--text pr-5 pl-2" size="40">mdi-play-network-outline</v-icon>
|
||||
Solution </v-card-title>
|
||||
<v-container class="text-center">
|
||||
<v-row justify="center">
|
||||
<v-container class="text-center"> <!-- Utilisation de styles CSS personnalisés pour centrer l'image -->
|
||||
<v-img width="450" src="@/assets/copilot-solution-FULL-HD.jpg" style="margin: 0 auto;">
|
||||
</v-img>
|
||||
</v-container>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</template>
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.image-container {
|
||||
width: 300px; overflow: hidden; /* Pour masquer le dépassement de l'image */
|
||||
border: 1px solid #ccc; /* Bordure de l'image */
|
||||
}
|
||||
.image-container img {
|
||||
width: 100%; /* Pour remplir complètement le conteneur */
|
||||
height: auto; /* Pour maintenir le ratio d'aspect de l'image */
|
||||
display: block; /* Pour éviter l'espace réservé pour les images */
|
||||
}
|
||||
}
|
||||
.card--reduced {
|
||||
height: 56px; /* Réglez la hauteur réduite selon vos besoins */
|
||||
width: 160px;
|
||||
overflow: hidden;
|
||||
transition: height 0.6s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
// Variable pour contrôler l'état de la carte
|
||||
const isCardReduced = ref(false);
|
||||
|
||||
// Méthode pour basculer l'état de la carte
|
||||
function toggleCardSize() {
|
||||
isCardReduced.value = !isCardReduced.value;
|
||||
}
|
||||
</script>
|
||||
61
VApp/src/components/CardSoundboard.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<v-card tile outlined :class="{ 'card--reduced': isCardReduced }">
|
||||
<v-card-title class="card__title primary" @click="toggleCardSize">
|
||||
<v-icon left class="white--text pr-5 pl-2" size="40">mdi-music-box-multiple</v-icon>
|
||||
Soundboard
|
||||
</v-card-title>
|
||||
<v-container class="text-center">
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="6" md="4" class="mt-4">
|
||||
<mqtt-button class="btn red" width="150" height="90" topic="/sound/playsound" message="good-response" rounded>
|
||||
<v-icon size="60">mdi-check-circle-outline</v-icon>
|
||||
</mqtt-button>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" class="mt-4">
|
||||
<mqtt-button class="btn red" width="150" height="90" topic="/sound/playsound" message="bad-response" rounded>
|
||||
<v-icon size="60">mdi-close-circle-outline</v-icon>
|
||||
</mqtt-button>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" class="mt-4">
|
||||
<mqtt-button class="btn red" width="150" height="90" topic="/sound/playsound" message="timer" rounded>
|
||||
<v-icon size="60">mdi-timer-outline</v-icon>
|
||||
</mqtt-button>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" sm="6" md="4" class="mb-4">
|
||||
<mqtt-button class="btn red" width="150" height="90" topic="/sound/playsound" message="applause" rounded>
|
||||
<v-icon size="60">mdi-human-handsup</v-icon>
|
||||
</mqtt-button>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" class="mb-4">
|
||||
<mqtt-button class="btn red" width="150" height="90" topic="/sound/playsound" message="bell" rounded>
|
||||
<v-icon size="60">mdi-bell-outline</v-icon>
|
||||
</mqtt-button>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MqttButton from './MqttButton.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
// Variable pour contrôler l'état de la carte
|
||||
const isCardReduced = ref(false);
|
||||
|
||||
// Méthode pour basculer l'état de la carte
|
||||
function toggleCardSize() {
|
||||
isCardReduced.value = !isCardReduced.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card--reduced {
|
||||
height: 56px; /* Réglez la hauteur réduite selon vos besoins */
|
||||
width: 190px;
|
||||
overflow: hidden;
|
||||
transition: height 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
100
VApp/src/components/CardTimer.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="timer">
|
||||
<v-label color="primary" class="labelTime-style" >{{ formatTime }}</v-label>
|
||||
</div>
|
||||
<v-row no-gutters justify="space-around" >
|
||||
<v-btn class="buttons" color="primary" icon="mdi-play" @click="startTimer"></v-btn>
|
||||
<v-btn color="primary" icon="mdi-pause" @click="pauseTimer"></v-btn>
|
||||
<v-btn color="primary" icon="mdi-restart" @click="resetTimer"></v-btn>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onBeforeUnmount } from 'vue';
|
||||
|
||||
const timerActive = ref(false);
|
||||
const startTime = ref(null);
|
||||
const currentTime = ref(null);
|
||||
const elapsedTime = ref(0);
|
||||
|
||||
const formatTime = computed(() => {
|
||||
let seconds = Math.floor(elapsedTime.value / 1000);
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
seconds = seconds % 60; minutes = minutes % 60;
|
||||
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
||||
});
|
||||
|
||||
const pad = (number) => {
|
||||
return (number < 10 ? "0" : "") + number;
|
||||
};
|
||||
|
||||
const startTimer = () => {
|
||||
if (!timerActive.value) {
|
||||
timerActive.value = true;
|
||||
startTime.value = Date.now() - elapsedTime.value;
|
||||
updateTimer(); }
|
||||
};
|
||||
|
||||
const pauseTimer = () => {
|
||||
if (timerActive.value) {
|
||||
timerActive.value = false;
|
||||
clearInterval(currentTime.value); }
|
||||
};
|
||||
|
||||
const resetTimer = () => {
|
||||
elapsedTime.value = 0;
|
||||
timerActive.value = false;
|
||||
clearInterval(currentTime.value);
|
||||
};
|
||||
|
||||
const updateTimer = () => {
|
||||
currentTime.value = setInterval(() => {elapsedTime.value = Date.now() - startTime.value; }, 1000);
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => { clearInterval(currentTime.value);
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
const startTimer = () => {
|
||||
if (!timerActive.value) {
|
||||
timerActive.value = true;
|
||||
startTime.value = Date.now() - elapsedTime.value;
|
||||
updateTimer(); } };
|
||||
const pauseTimer = () => {
|
||||
if (timerActive.value) {
|
||||
timerActive.value = false;
|
||||
clearInterval(currentTime.value); } };
|
||||
const resetTimer = () => {
|
||||
elapsedTime.value = 0;
|
||||
timerActive.value = false;
|
||||
clearInterval(currentTime.value); };
|
||||
export { startTimer, pauseTimer, resetTimer };
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
text-align: center;
|
||||
margin-top: auto; /* Place le container en bas de son parent */
|
||||
margin-bottom: 1px; /* Marge en bas pour un espacement */
|
||||
position: fixed; /* Le positionne de manière fixe */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.timer {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.labelTime-style {
|
||||
font-size: 30px !important;
|
||||
font-weight: 500;
|
||||
color: #d42828 !important;
|
||||
opacity: 90% !important;
|
||||
}
|
||||
.buttons{
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
</style>
|
||||
19
VApp/src/components/GameStatus.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<v-navigation-drawer width="250">
|
||||
<BuzzerWatcherCard/>
|
||||
<CardScore/>
|
||||
<CardCurrentQuizz/>
|
||||
<CardTimer/>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import CardTimer from '@/components/CardTimer.vue'
|
||||
import CardScore from '@/components/CardScore.vue'
|
||||
import BuzzerWatcherCard from '@/components/BuzzerWatcherCard.vue'
|
||||
import CardCurrentQuizz from '@/components/CardCurrentQuizz.vue'
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
118
VApp/src/components/MQTTColorPublisher.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<v-container class="v-container-style">
|
||||
<v-card tile outlined width="500">
|
||||
<v-card-title class="card__title primary centered-title">
|
||||
<v-icon left class="pr-5 pl-2" size="30">mdi-send</v-icon>
|
||||
Publier une couleur
|
||||
</v-card-title>
|
||||
|
||||
<div class="input-style">
|
||||
<v-select
|
||||
label="Topic"
|
||||
v-model="selectedTopic"
|
||||
:items="topics"
|
||||
prepend-icon="mdi-target"
|
||||
></v-select>
|
||||
</div>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="6" class="color-picker-style">
|
||||
<div>
|
||||
<v-color-picker
|
||||
mode="hex"
|
||||
v-model="selectedColor"
|
||||
border="md"
|
||||
width="250"
|
||||
></v-color-picker>
|
||||
<v-btn
|
||||
class="v-btn-style-validate"
|
||||
height="35"
|
||||
@click="publishCustomColor"
|
||||
>
|
||||
Publier
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="4" class="button-container-2">
|
||||
<v-btn color="#D42828" class="team-button" @click="publishButtonColor('#D42828')">Team Rouge</v-btn>
|
||||
<v-btn color="#00FF1F" class="team-button" @click="publishButtonColor('#00FF1F')">Team Verte</v-btn>
|
||||
<v-btn color="#007AFF" class="team-button" @click="publishButtonColor('#007AFF')">Team Bleue</v-btn>
|
||||
<v-btn color="#FFFC00" class="team-button" @click="publishButtonColor('#FFFC00')">Team Jaune</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { publishMessage } from '@/services/mqttService';
|
||||
|
||||
const selectedTopic = ref('Selectionnez un topic');
|
||||
const selectedColor = ref('#FF0000');
|
||||
const topics = ref([
|
||||
'/wled/all',
|
||||
'/wled/1',
|
||||
'/wled/2',
|
||||
'/wled/3',
|
||||
'/wled/4',
|
||||
'/wled/5',
|
||||
]);
|
||||
|
||||
const publishCustomColor = () => {
|
||||
if (selectedTopic.value && selectedColor.value) {
|
||||
publishMessage(selectedTopic.value, selectedColor.value);
|
||||
} else {
|
||||
console.warn('Topic ou couleur non sélectionné !');
|
||||
}
|
||||
};
|
||||
|
||||
const publishButtonColor = (color) => {
|
||||
if (selectedTopic.value) {
|
||||
publishMessage(selectedTopic.value, color);
|
||||
} else {
|
||||
console.warn('Topic non sélectionné !');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-container-style {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.input-style {
|
||||
margin: 20px;
|
||||
}
|
||||
.v-btn-style-validate {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
background-color: rgba(var(--v-theme-primary));
|
||||
border-top-right-radius: 0%;
|
||||
border-top-left-radius: 0%;
|
||||
}
|
||||
.centered-title {
|
||||
text-align: center;
|
||||
}
|
||||
.color-picker-style {
|
||||
margin-left: 5%;
|
||||
margin-bottom: 5%;
|
||||
display: flex;
|
||||
}
|
||||
.button-container-2 {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 27px;
|
||||
margin-left: 15px;
|
||||
margin-right: 13px;
|
||||
}
|
||||
.team-button {
|
||||
width: 140px;
|
||||
display: block;
|
||||
margin: 12% auto 0;
|
||||
background-color: rgba(var(--v-theme-primary));
|
||||
}
|
||||
</style>
|
||||
136
VApp/src/components/MQTTDebugConsole.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<v-container class="v-container-style-console">
|
||||
<v-card tile outlined width="500">
|
||||
<v-card-title class="card__title primary centered-title">
|
||||
<v-icon left class="pr-5 pl-2" size="40">mdi-console-line</v-icon>
|
||||
Console MQTT
|
||||
</v-card-title>
|
||||
|
||||
<div class="div_topic">
|
||||
<v-select
|
||||
density="compact"
|
||||
label="Topic"
|
||||
v-model="selectedTopic"
|
||||
:items="topics"
|
||||
prepend-icon="mdi-target"
|
||||
></v-select>
|
||||
|
||||
<div class="button_div_style">
|
||||
<v-btn rounded @click="resetTopicFilter" color="primary">Unfilter</v-btn>
|
||||
<v-btn
|
||||
rounded
|
||||
:class="{ 'scrolling-paused': scrollingState, 'scrolling-active': !scrollingState }"
|
||||
@click="toggleScrollingState"
|
||||
>
|
||||
<v-icon>{{ scrollingState ? 'mdi-pause' : 'mdi-play' }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-container class="text-center">
|
||||
<div v-for="(log, index) in filteredLogs" :key="index">
|
||||
<v-label class="v-label-timestamp">{{ log.timestamp }} - </v-label>
|
||||
<v-label class="v-label-topic-message-title">Topic : </v-label>
|
||||
<v-label class="v-label-topic-message">{{ log.topic }} </v-label>
|
||||
<v-label class="v-label-topic-message-title">Msg : </v-label>
|
||||
<v-label class="v-label-topic-message">{{ log.message }}</v-label>
|
||||
</div>
|
||||
</v-container>
|
||||
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { subscribeToTopic } from '@/services/mqttService';
|
||||
|
||||
// Data
|
||||
const messageLogs = ref([]);
|
||||
const selectedTopic = ref('');
|
||||
const topics = ref([]);
|
||||
const scrollingState = ref(true);
|
||||
|
||||
// Methods
|
||||
const handleMessage = (topic, message) => {
|
||||
const timestamp = new Date().toLocaleString('fr-FR', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
|
||||
if (scrollingState.value) {
|
||||
if (!topics.value.includes(topic)) {
|
||||
topics.value.push(topic);
|
||||
}
|
||||
|
||||
messageLogs.value.unshift({ timestamp, topic, message });
|
||||
|
||||
if (messageLogs.value.length > 20) {
|
||||
messageLogs.value.pop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetTopicFilter = () => {
|
||||
selectedTopic.value = '';
|
||||
};
|
||||
|
||||
const toggleScrollingState = () => {
|
||||
scrollingState.value = !scrollingState.value;
|
||||
};
|
||||
|
||||
// Computed
|
||||
const filteredLogs = computed(() => {
|
||||
if (!selectedTopic.value) {
|
||||
return messageLogs.value;
|
||||
}
|
||||
return messageLogs.value.filter((log) => log.topic === selectedTopic.value);
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
subscribeToTopic('#', (topic, message) => {
|
||||
handleMessage(topic, message);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scrolling-paused {
|
||||
background-color: rgba(var(--v-theme-primary)) !important;
|
||||
}
|
||||
.scrolling-active {
|
||||
background-color: rgba(var(--v-theme-success)) !important;
|
||||
}
|
||||
.button_div_style {
|
||||
justify-content: space-evenly;
|
||||
display: flex;
|
||||
}
|
||||
.div_topic {
|
||||
text-align: center !important;
|
||||
padding: 5% 4% 1% 4%;
|
||||
}
|
||||
.v-container-style-console {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
top: 50px;
|
||||
}
|
||||
.centered-title {
|
||||
text-align: center;
|
||||
}
|
||||
.v-label-timestamp {
|
||||
opacity: 100%;
|
||||
font-style: oblique;
|
||||
font-weight: 400;
|
||||
color: #838383;
|
||||
}
|
||||
.v-label-topic-message-title {
|
||||
opacity: 100%;
|
||||
font-weight: 700;
|
||||
color: rgba(var(--v-theme-primary));
|
||||
}
|
||||
.v-label-topic-message {
|
||||
font-weight: 300;
|
||||
}
|
||||
</style>
|
||||
93
VApp/src/components/MQTTDebugPublish.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<v-container class="v-container-style">
|
||||
<v-card tile outlined width="500">
|
||||
<v-card-title class="card__title primary centered-title">
|
||||
<v-icon left class="pr-5 pl-2" size="30">mdi-send</v-icon>
|
||||
Publier un message
|
||||
</v-card-title>
|
||||
|
||||
<div class="input-style">
|
||||
<v-select
|
||||
label="Topic"
|
||||
v-model="selectedTopic"
|
||||
:items="topics"
|
||||
prepend-icon="mdi-target"
|
||||
></v-select>
|
||||
|
||||
<v-text-field
|
||||
label="Message"
|
||||
v-model="message"
|
||||
prepend-icon="mdi-text-box"
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
rounded
|
||||
class="v-btn-style-standalone"
|
||||
height="40"
|
||||
@click="publishBuzzerUnblock"
|
||||
>
|
||||
Déblocage<br>Buzzer
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
class="v-btn-style-validate"
|
||||
height="50"
|
||||
@click="publisCustomMessage"
|
||||
>
|
||||
Publier
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { publishMessage } from '@/services/mqttService';
|
||||
|
||||
// Reactive data
|
||||
const message = ref('');
|
||||
const selectedTopic = ref('Selectionnez un topic');
|
||||
const topics = [
|
||||
'wled/all',
|
||||
'display/control',
|
||||
'sound/playsound',
|
||||
'game/score/update',
|
||||
'game/score'
|
||||
];
|
||||
|
||||
// Methods
|
||||
const publisCustomMessage = () => {
|
||||
publishMessage(selectedTopic.value, message.value);
|
||||
};
|
||||
|
||||
const publishBuzzerUnblock = () => {
|
||||
publishMessage('brainblast/buzzer/unlock', "0");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-container-style {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.input-style {
|
||||
margin: 20px;
|
||||
}
|
||||
.v-btn-style-standalone {
|
||||
background-color: rgba(var(--v-theme-primary));
|
||||
margin-bottom: 5%;
|
||||
margin-left: 5%;
|
||||
}
|
||||
.v-btn-style-validate {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
background-color: rgba(var(--v-theme-primary));
|
||||
border-top-right-radius: 0%;
|
||||
border-top-left-radius: 0%;
|
||||
}
|
||||
.centered-title {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
22
VApp/src/components/MqttButton.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<v-btn @click="_publishMessage" v-bind="$attrs">
|
||||
<slot/>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { publishMessage } from '@/services/mqttService'
|
||||
import { ref, defineProps } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
topic: String,
|
||||
message: null
|
||||
})
|
||||
|
||||
const disabled = ref(false)
|
||||
|
||||
const _publishMessage = () => {
|
||||
publishMessage(props.topic, props.message)
|
||||
disabled.value = true
|
||||
}
|
||||
</script>
|
||||
29
VApp/src/components/RouterMenu.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<v-app-bar-nav-icon v-on:click="menu = !menu"></v-app-bar-nav-icon>
|
||||
<v-menu v-model="menu" class="menu-below-bar">
|
||||
<v-list>
|
||||
<v-list-item v-for="route in filteredRoutes" :key="route.name" :to="route.path">{{ route.name }}</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const routes = router.options.routes;
|
||||
|
||||
let menu = ref(false);
|
||||
|
||||
// Filtrer les routes pour masquer une route spécifique (par exemple, 'RouteA')
|
||||
const filteredRoutes = computed(() => {
|
||||
return routes.filter(route => route.name !== 'Debugger MQTT');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-below-bar {
|
||||
margin-top: 48px; /* La hauteur de la barre d'application */
|
||||
}
|
||||
</style>
|
||||
102
VApp/src/components/WSDebugControl.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<v-container class="v-container-style-console-dmx">
|
||||
<v-card tile outlined width="500">
|
||||
<v-card-title class="card__title primary centered-title">
|
||||
<v-icon left class="pr-5 pl-2" size="40">mdi-console-line</v-icon>
|
||||
Console DMX
|
||||
</v-card-title>
|
||||
<v-container class="text-center">
|
||||
<v-label v-if="!isConnected">Connecting...</v-label>
|
||||
<div class="button-container">
|
||||
<v-btn class="v-btn-dmx white" @click="handleButtonPress(2, 1)" :disabled="!isConnected">Rouge</v-btn>
|
||||
<v-btn class="v-btn-dmx red" @click="handleButtonPress(7, 1)" :disabled="!isConnected">Rouge</v-btn>
|
||||
<v-btn class="v-btn-dmx green" @click="handleButtonPress(8, 1)" :disabled="!isConnected">Vert</v-btn>
|
||||
<v-btn class="v-btn-dmx blue" @click="handleButtonPress(9, 1)" :disabled="!isConnected">Blue</v-btn>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { connectWebSocket, sendButtonPress } from '@/services/light-manager-DMX.js';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const websocketUrl = 'ws://192.168.1.30:9999/qlcplusWS';
|
||||
const isConnected = ref(false);
|
||||
|
||||
// Fonction pour connecter le WebSocket et mettre à jour l'état de la connexion
|
||||
const connect = () => {
|
||||
connectWebSocket(websocketUrl);
|
||||
isConnected.value = true; // Mettre à jour l'état de la connexion
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
connect();
|
||||
});
|
||||
|
||||
const handleButtonPress = (id, state) => {
|
||||
sendButtonPress(id, state);
|
||||
// Pause de 2 secondes
|
||||
setTimeout(() => {
|
||||
sendButtonPress(id, !state);
|
||||
}, 500); // 2000 millisecondes = 2 secondes
|
||||
|
||||
};
|
||||
|
||||
return {
|
||||
handleButtonPress,
|
||||
isConnected,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-container-style-console-dmx {
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
top: 50px; /* Distance depuis le haut de la fenêtre avant de "coller" */
|
||||
}
|
||||
.centered-title {
|
||||
text-align: center;
|
||||
}
|
||||
.v-label-timestamp{
|
||||
opacity: 100%;
|
||||
font-style: oblique;
|
||||
font-weight: 400;
|
||||
color: #838383;
|
||||
}
|
||||
.button-container{
|
||||
display: flex; /* Active le mode Flexbox */
|
||||
justify-content: space-around; /* Répartit les boutons équitablement */
|
||||
align-items: center; /* Aligne les boutons verticalement */
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 27px;
|
||||
margin-left: 15px;
|
||||
margin-right: 13px;
|
||||
}
|
||||
.v-btn-dmx{
|
||||
width: 100px;
|
||||
margin: 2% auto 0; /* 5% de marge en bas pour espacer les boutons */
|
||||
|
||||
}
|
||||
.v-btn-dmx.white{
|
||||
background-color:#ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
.v-btn-dmx.red{
|
||||
background-color: rgba(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.v-btn-dmx.green{
|
||||
background-color: rgba(var(--v-theme-GreenBuzzer));
|
||||
}
|
||||
|
||||
.v-btn-dmx.blue{
|
||||
background-color: rgba(var(--v-theme-BlueBuzzer));
|
||||
}
|
||||
</style>
|
||||
16
VApp/src/config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// Fichier vide, regarde config.js.example pour personaliser ce fichier.
|
||||
// Note de dev : Normalement ce fichier ne devrait plus avoir de
|
||||
// modifications
|
||||
|
||||
// config.js
|
||||
export default {
|
||||
mqttBrokerUrl: 'ws://192.168.1.30:9001',
|
||||
|
||||
// Buzzer
|
||||
redBuzzerIP: '192.168.73.40',
|
||||
blueBuzzerIP: '192.168.73.41',
|
||||
orangeBuzzerIP: '192.168.73.42',
|
||||
greenBuzzerIP: '192.168.73.43'
|
||||
|
||||
// Light
|
||||
};
|
||||
4
VApp/src/config.js.example
Normal file
@@ -0,0 +1,4 @@
|
||||
// config.js
|
||||
export default {
|
||||
mqttBrokerUrl: 'ws://localhost:9001'
|
||||
};
|
||||
9
VApp/src/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
registerPlugins(app)
|
||||
|
||||
app.mount('#app')
|
||||
3
VApp/src/plugins/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Plugins
|
||||
|
||||
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.
|
||||
14
VApp/src/plugins/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* plugins/index.js
|
||||
*
|
||||
* Automatically included in `./src/main.js`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import vuetify from './vuetify'
|
||||
import router from './router'
|
||||
|
||||
export function registerPlugins (app) {
|
||||
app.use(vuetify)
|
||||
app.use(router)
|
||||
}
|
||||
33
VApp/src/plugins/router.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [ {
|
||||
path: '/',
|
||||
name: 'Accueil',
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/game/control',
|
||||
name: 'Game Control (Présentateur)',
|
||||
component: () => import('@/views/GameControl.vue')
|
||||
},
|
||||
{
|
||||
path: '/game/display',
|
||||
name: 'Game Display (Projection)',
|
||||
component: () => import('@/views/GameDisplay.vue')
|
||||
},
|
||||
{
|
||||
path: '/mqtt-debugger',
|
||||
name: 'Debugger MQTT',
|
||||
component: () => import('@/views/MQTTDebugView.vue')
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Paramètres',
|
||||
component: () => import('@/views/SettingsView.vue') }
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
59
VApp/src/plugins/vuetify.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* plugins/vuetify.js
|
||||
*
|
||||
* Framework documentation: https://vuetifyjs.com`
|
||||
*/
|
||||
|
||||
// Styles
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Composables
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
const CustomThemeDark = {
|
||||
dark: true,
|
||||
colors: {
|
||||
background: '#121212',
|
||||
primary: '#d42828',
|
||||
secondary: '#F44336',
|
||||
accent: '#FFC107',
|
||||
error: '#e91e1e',
|
||||
warning: '#FFC107',
|
||||
info: '#607D8B',
|
||||
success: '#15B01B',
|
||||
BlueBuzzer: '#2867d4',
|
||||
YellowBuzzer: '#D4D100',
|
||||
RedBuzzer: '#d42828',
|
||||
GreenBuzzer: '#28d42e',
|
||||
DisconnectedBuzzer: '#595959',
|
||||
}
|
||||
}
|
||||
const CustomThemeLight = {
|
||||
dark: false,
|
||||
colors: {
|
||||
background: '#ffffff',
|
||||
primary: '#d42828',
|
||||
secondary: '#F44336',
|
||||
accent: '#FFC107',
|
||||
error: '#e91e1e',
|
||||
warning: '#FFC107',
|
||||
info: '#607D8B',
|
||||
success: '#4CAF50',
|
||||
BlueBuzzer: '#2867d4',
|
||||
YellowBuzzer: '#D4D100',
|
||||
RedBuzzer: '#d42828',
|
||||
GreenBuzzer: '#28d42e',
|
||||
DisconnectedBuzzer: '#595959',
|
||||
}
|
||||
}
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
theme: {
|
||||
defaultTheme: 'CustomThemeDark',
|
||||
themes: {
|
||||
CustomThemeDark,
|
||||
CustomThemeLight, },
|
||||
},
|
||||
})
|
||||
BIN
VApp/src/quizz/Quizz-1/festival/MysteryLand.jpg
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
VApp/src/quizz/Quizz-1/festival/Mysteryland_h264.mp4
Normal file
BIN
VApp/src/quizz/Quizz-1/geography-history/Q-1.jpeg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
VApp/src/quizz/Quizz-1/geography-history/Q-2.jpeg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
VApp/src/quizz/Quizz-1/geography-history/Q-3.jpeg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
VApp/src/quizz/Quizz-1/geography-history/Q-4.jpeg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
VApp/src/quizz/Quizz-1/geography-history/Q-5.jpeg
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
27
VApp/src/quizz/Quizz-1/geography-history/config.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: "Histoire & Géographie"
|
||||
questions:
|
||||
1:
|
||||
Q: "Quelle bataille célèbre s'est déroulée en 1815, marquant la défaite de Napoléon Bonaparte ?"
|
||||
T: "Elle a eu lieu en Belgique"
|
||||
R: "La bataille de Waterloo."
|
||||
P: "Q-1.jpeg"
|
||||
2:
|
||||
Q: "Quelle est la capitale de l'Australie ?"
|
||||
T: "Le nom de cette ville commence par la lettre 'C'"
|
||||
R: "Canberra."
|
||||
P: "Q-2.jpeg"
|
||||
3:
|
||||
Q: "En quelle année la Seconde Guerre mondiale a-t-elle pris fin ?"
|
||||
T: "C'est au milieu des années 40."
|
||||
R: "En 1945."
|
||||
P: "Q-3.jpeg"
|
||||
4:
|
||||
Q: "Quel fleuve traverse la ville du Caire en Égypte ?"
|
||||
T: "C'est l'un des plus longs fleuves du monde"
|
||||
R: "Le Nil."
|
||||
P: "Q-4.jpeg"
|
||||
5:
|
||||
Q: "Quel pays a été divisé par un mur de 1961 à 1989 ?"
|
||||
T: "Sa chute a marqué la fin de la guerre froide."
|
||||
R: "L'Allemagne (le mur de Berlin)."
|
||||
P: "Q-5.jpeg"
|
||||
BIN
VApp/src/quizz/Quizz-1/geography-history/originales/Q-1.webp
Normal file
|
After Width: | Height: | Size: 447 KiB |
BIN
VApp/src/quizz/Quizz-1/geography-history/originales/Q-2.webp
Normal file
|
After Width: | Height: | Size: 460 KiB |
BIN
VApp/src/quizz/Quizz-1/geography-history/originales/Q-3.webp
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
VApp/src/quizz/Quizz-1/geography-history/originales/Q-4.webp
Normal file
|
After Width: | Height: | Size: 355 KiB |
BIN
VApp/src/quizz/Quizz-1/geography-history/originales/Q-5.webp
Normal file
|
After Width: | Height: | Size: 521 KiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Original/Q-1.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Original/Q-2.jpg
Normal file
|
After Width: | Height: | Size: 445 KiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Original/Q-3.webp
Normal file
|
After Width: | Height: | Size: 400 KiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Original/Q-4.webp
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Original/Q-5.webp
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Q-1.jpeg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Q-2.jpeg
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Q-3.jpeg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Q-4.jpeg
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
VApp/src/quizz/Quizz-1/video-games/Q-5.jpeg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
27
VApp/src/quizz/Quizz-1/video-games/config.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: "Jeux vidéos"
|
||||
questions:
|
||||
1:
|
||||
Q: "Quel personnage de jeu vidéo est un plombier moustachu qui saute sur des ennemis pour sauver une princesse ?"
|
||||
T: "C’est le personnage le plus célèbre de Nintendo, son nom commence par 'M'."
|
||||
R: "Mario."
|
||||
P: "Q-1.jpeg"
|
||||
2:
|
||||
Q: "Quel jeu vidéo multijoueur de football avec des voitures est très populaire ?"
|
||||
T: "Il s'agit d'un mélange de sport et de voitures rapides."
|
||||
R: "Rocket League."
|
||||
P: "Q-2.jpeg"
|
||||
3:
|
||||
Q: "Quel jeu vidéo mobile consiste à faire exploser des bonbons en alignant trois pièces identiques ?"
|
||||
T: "Son nom fait référence aux bonbons."
|
||||
R: "Candy Crush Saga."
|
||||
P: "Q-3.jpeg"
|
||||
4:
|
||||
Q: "Quel est le nom du célèbre personnage bleu de SEGA qui court à une vitesse incroyable ?"
|
||||
T: "Son nom commence par la lettre 'S' et c'est un hérisson."
|
||||
R: "Sonic"
|
||||
P: "Q-4.jpeg"
|
||||
5:
|
||||
Q: "Quel jeu permet de construire et explorer un monde fait de blocs, tout en survivant face à des monstres ?"
|
||||
T: "Le monde est entièrement fait de blocs carrés."
|
||||
R: "Minecraft."
|
||||
P: "Q-5.jpeg"
|
||||
69
VApp/src/services/light-manager-DMX.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// websocket.js
|
||||
let websocket;
|
||||
|
||||
export function connectWebSocket(url) {
|
||||
websocket = new WebSocket(url);
|
||||
|
||||
websocket.onopen = () => {
|
||||
console.log("WebSocket connection established");
|
||||
};
|
||||
|
||||
websocket.onclose = () => {
|
||||
console.log("QLC+ connection is closed. Reconnect will be attempted in 1 second.");
|
||||
setTimeout(() => {
|
||||
connectWebSocket(url);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
websocket.onerror = (err) => {
|
||||
console.error("QLC+ connection encountered error. Closing socket", err);
|
||||
websocket.close();
|
||||
};
|
||||
|
||||
websocket.onmessage = (ev) => {
|
||||
const message = ev.data.toString();
|
||||
const msgParams = message.split("|");
|
||||
|
||||
if (msgParams[1] === "BUTTON") {
|
||||
//wsSetButtonState(msgParams[0], msgParams[2]);
|
||||
} else if (msgParams[1] === "SLIDER") {
|
||||
wsSetSliderValue(msgParams[0], msgParams[2], msgParams[3]);
|
||||
} else if (msgParams[1] === "AUDIOTRIGGERS") {
|
||||
wsSetAudioTriggersEnabled(msgParams[0], msgParams[2]);
|
||||
} else if (msgParams[1] === "CUE") {
|
||||
wsSetCueIndex(msgParams[0], msgParams[2]);
|
||||
} else if (msgParams[1] === "CLOCK") {
|
||||
wsUpdateClockTime(msgParams[0], msgParams[2]);
|
||||
} else if (msgParams[1] === "FRAME") {
|
||||
setFramePage(msgParams[0], msgParams[2]);
|
||||
} else if (msgParams[1] === "getWidgetsList") {
|
||||
console.log(msgParams)
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function sendButtonPress(id, state) {
|
||||
if (state == 1){
|
||||
websocket.send(id + "|255");
|
||||
// Quand un bouton est pressé, envoyez un message au serveur
|
||||
console.log(`Button ${id} pressed and sent to server`);
|
||||
}
|
||||
if (state == 0){
|
||||
websocket.send(id + "|0");
|
||||
// Quand un bouton est pressé, envoyez un message au serveur
|
||||
console.log(`Button ${id} released and sent to server`);
|
||||
}
|
||||
}
|
||||
|
||||
export function sendCMD(cmd) {
|
||||
websocket.send("QLC+CMD|" + cmd);
|
||||
}
|
||||
|
||||
export function requestAPI(cmd)
|
||||
{
|
||||
if (isConnected === true)
|
||||
websocket.send("QLC+API|" + cmd);
|
||||
else
|
||||
alert("You must connect to QLC+ WebSocket first!");
|
||||
}
|
||||
18
VApp/src/services/mqttService.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import mqtt from 'mqtt'
|
||||
import config from '@/config.js'
|
||||
|
||||
const mqttBrokerUrl = config.mqttBrokerUrl
|
||||
// Créer une instance de client MQTT
|
||||
const client = mqtt.connect(mqttBrokerUrl)
|
||||
|
||||
// Fonction pour publier un message sur un topic MQTT
|
||||
export function publishMessage(topic, message) {
|
||||
client.publish(topic, message)
|
||||
}
|
||||
|
||||
// Fonction pour s'abonner à un topic MQTT et écouter les messages entrants
|
||||
export function subscribeToTopic(topic, callback) {
|
||||
client.subscribe(topic)
|
||||
client.on('message', (receivedTopic, message) => { callback(receivedTopic.toString(), message.toString())
|
||||
})
|
||||
}
|
||||
43
VApp/src/services/pictureEngine.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import express from 'express';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
// Obtenir le chemin du répertoire parent
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Middleware pour gérer les requêtes depuis le frontend
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
|
||||
next();
|
||||
});
|
||||
|
||||
// Le dossier assets est situé un niveau au-dessus du dossier services
|
||||
const assetsDir = path.join(__dirname, '..', 'quizz/geography-history');
|
||||
console.log(assetsDir)
|
||||
|
||||
// Middleware pour servir les fichiers statiques
|
||||
app.use('/images', express.static(assetsDir));
|
||||
|
||||
// API pour lister les fichiers d'image
|
||||
app.get('/images-list', async (req, res) => {
|
||||
try {
|
||||
const files = await fs.readdir(assetsDir);
|
||||
|
||||
// Filtrer pour ne renvoyer que les fichiers d'image (par ex : .jpg, .png)
|
||||
const images = files.filter(file => /\.(jpg|jpeg|png|gif)$/.test(file));
|
||||
res.json(images);
|
||||
} catch (err) {
|
||||
|
||||
res.status(500).send('Erreur lors de la lecture du dossier');
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Serveur démarré sur http://localhost:${port}`);
|
||||
});
|
||||
27
VApp/src/variables.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export default {
|
||||
// Gestion des score et des Buzzers
|
||||
|
||||
// Scores totaux
|
||||
RedTotalScore: 11,
|
||||
BlueTotalScore: 22,
|
||||
GreenTotalScore: 33,
|
||||
OrangeTotalScore: 44,
|
||||
|
||||
// Score de la manche courante
|
||||
RedRoundScore: 1,
|
||||
BlueRoundScore: 2,
|
||||
OrangeRoundScore: 3,
|
||||
GreenRoundScore: 4,
|
||||
|
||||
//Etat des buzzer
|
||||
BuzzerRed: false,
|
||||
BuzzerBlue: false,
|
||||
BuzzerOrange: false,
|
||||
BuzzerGreen: false,
|
||||
|
||||
// Ajoutez d'autres variables globales ici
|
||||
};
|
||||
|
||||
// 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') || '',
|
||||
};
|
||||
77
VApp/src/views/GameControl.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row no-gutters>
|
||||
<v-col class="align-start">
|
||||
<card-control />
|
||||
</v-col>
|
||||
<v-col class="pl-3">
|
||||
<card-soundboard />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-row no-gutters class="pr-4 pl-4">
|
||||
<v-row no-gutters>
|
||||
<v-col class="align-start">
|
||||
<CardButtonScore />
|
||||
</v-col>
|
||||
<v-col class="pl-3">
|
||||
<card-solution />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import CardSolution from '@/components/CardSolution.vue'
|
||||
import CardControl from '@/components/CardControl.vue'
|
||||
import CardSoundboard from '@/components/CardSoundboard.vue';
|
||||
import CardButtonScore from '@/components/CardButtonScore.vue'
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.card__title.primary {
|
||||
background-color: #d42828; /* Changez la couleur en fonction de votre thème */
|
||||
}
|
||||
.card__title.feedback {
|
||||
background-color: #2E7D32; /* Changez la couleur en fonction de votre thème */
|
||||
}
|
||||
.btn{
|
||||
border-radius:20px!important;
|
||||
}
|
||||
.btn.red {
|
||||
background-color: #d42828; /* Changez la couleur en fonction de votre thème */
|
||||
}
|
||||
.btn.blue {
|
||||
background-color: #2867d4; /* Changez la couleur en fonction de votre thème */
|
||||
}
|
||||
.btn.yellow {
|
||||
background-color: #d4d100; /* Changez la couleur en fonction de votre thème */
|
||||
}
|
||||
.btn.green {
|
||||
background-color: #28d42e; /* Changez la couleur en fonction de votre thème */
|
||||
}
|
||||
.scorediv-style-red {
|
||||
background-color: #d42828 !important;
|
||||
padding: 15px;
|
||||
border-top-left-radius: 10%;
|
||||
}
|
||||
.scorediv-style-yellow {
|
||||
background-color: #d4d100!important;
|
||||
padding: 15px;
|
||||
border-bottom-left-radius: 10%;
|
||||
}
|
||||
.scorediv-style-blue {
|
||||
background-color: #2867d4 !important;
|
||||
padding: 15px;
|
||||
border-top-right-radius: 10%;
|
||||
}
|
||||
.scorediv-style-green {
|
||||
background-color: #28d42e !important;
|
||||
padding: 15px;
|
||||
border-bottom-right-radius: 10%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
188
VApp/src/views/GameDisplay.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="main_div">
|
||||
<div>
|
||||
<v-container class="score_div_main">
|
||||
<v-container class="score_div color-blue"></v-container>
|
||||
<v-container class="score_div color-red"></v-container>
|
||||
<v-container class="score_div color-white d-flex align-center justify-center">
|
||||
<span class="v-label-time">00:00</span>
|
||||
</v-container>
|
||||
<v-container class="score_div color-green"></v-container>
|
||||
<v-container class="score_div color-yellow"></v-container>
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<v-container v-show="gamehiding === true" class="v-container-game-hided">
|
||||
<v-img src="@\assets\v-hide.png" class="v-img-hidding"></v-img>
|
||||
</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>
|
||||
</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';
|
||||
|
||||
// --- Déclarations
|
||||
const player = ref(null);
|
||||
let gamehiding = ref(true)
|
||||
|
||||
const videoOptions = {
|
||||
autoplay: false,
|
||||
controls: false,
|
||||
preload: 'auto',
|
||||
fluid: true,
|
||||
loop: true,
|
||||
volume: 0,
|
||||
sources: [{ src: Mysteryland_h264, type: 'video/mp4' }],
|
||||
};
|
||||
|
||||
// --- 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(() => {
|
||||
if (player.value) {
|
||||
player.value.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main_div {
|
||||
width: 100vw;
|
||||
height: calc(100vh - 15px);
|
||||
text-align: center;
|
||||
}
|
||||
.score_div_main {
|
||||
width: 60%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
background-color: rgb(80, 80, 80);
|
||||
padding: 25px 30px;
|
||||
border-radius: 0px 0px 30px 30px;
|
||||
box-shadow: 0px 1px 5px rgb(255, 0, 0);
|
||||
}
|
||||
.score_div {
|
||||
height: 100px;
|
||||
width: 170px;
|
||||
text-align: center;
|
||||
}
|
||||
.color-blue {
|
||||
background-color: rgb(var(--v-theme-BlueBuzzer), 1);
|
||||
border-radius: 40px 5px 40px 5px;
|
||||
}
|
||||
.color-red {
|
||||
background-color: rgb(var(--v-theme-RedBuzzer), 1);
|
||||
border-radius: 40px 5px 40px 5px;
|
||||
}
|
||||
.color-green {
|
||||
background-color: rgb(var(--v-theme-GreenBuzzer), 1);
|
||||
border-radius: 5px 40px 5px 40px;
|
||||
}
|
||||
.color-yellow {
|
||||
background-color: rgb(var(--v-theme-YellowBuzzer), 1);
|
||||
border-radius: 5px 40px 5px 40px;
|
||||
}
|
||||
.color-white {
|
||||
background-color: white;
|
||||
border-radius: 40px;
|
||||
}
|
||||
.v-label-time {
|
||||
padding-top: 5px;
|
||||
color: black;
|
||||
font-size: 49px;
|
||||
font-family: 'Bahnschrift';
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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>
|
||||
3
VApp/src/views/HomeView.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<v-img src="../assets/BrainBlast-For-HomeView-Alpha.png" class="fill-height"></v-img>
|
||||
</template>
|
||||
39
VApp/src/views/MQTTDebugView.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<!-- Colonne gauche avec les trois composants empilés -->
|
||||
<v-col cols="6" class="d-flex flex-column" style="align-items: stretch; height: 100%;">
|
||||
<PublishMQTTComponent />
|
||||
<MQTTColorPublisher />
|
||||
<WSDebugControl />
|
||||
</v-col>
|
||||
|
||||
<!-- Colonne droite avec le composant unique -->
|
||||
<v-col cols="6">
|
||||
<MQTTConsoleComponent />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PublishMQTTComponent from '@/components/MQTTDebugPublish.vue';
|
||||
import MQTTConsoleComponent from '@/components/MQTTDebugConsole.vue';
|
||||
import MQTTColorPublisher from '@/components/MQTTColorPublisher.vue';
|
||||
import WSDebugControl from '@/components/WSDebugControl.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@media (min-width: 1024px) {
|
||||
.card__title.primary {
|
||||
background-color: rgba(var(--v-theme-primary)); /* Couleur basée sur le thème */
|
||||
}
|
||||
.card__title.feedback {
|
||||
background-color: rgba(var(--v-theme-success)); /* Couleur basée sur le thème */
|
||||
}
|
||||
.btn {
|
||||
border-radius: 30px !important;
|
||||
background-color: rgba(var(--v-theme-primary)); /* Couleur basée sur le thème */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
117
VApp/src/views/SettingsView.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<v-label class="title-style-1">Paramètres</v-label>
|
||||
<v-divider :thickness="2" class="border-opacity-100" color="primary" />
|
||||
|
||||
<v-label class="title-style-2">Son</v-label>
|
||||
<div class="mutltiple-per-line">
|
||||
<v-switch hide-details label="Activer le son intégré" v-model="embeddedSound" class="ml-15" color="primary" />
|
||||
<div>
|
||||
<v-slider hide-details class="v-slider-style ml-15" :disabled="!embeddedSound" v-model="embeddedSoundVolume" color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-switch label="Activer le son MQTT" v-model="mqttSound" class="ml-15" color="primary" />
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-label class="title-style-2">Affichage</v-label>
|
||||
<div>
|
||||
<v-switch hide-details label="Activer l'affichage des satellites" v-model="satellitesDisplay" class="ml-15 pb-3" color="primary" />
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-label class="title-style-2">MQTT</v-label>
|
||||
<div class="mutltiple-per-line">
|
||||
<v-icon v-model="mqttBrokerState" class="ml-15 mb-5" color="error" icon="mdi-record" />
|
||||
<v-label class="ml-2 mb-10 mt-5">Etat du serveur MQTT</v-label>
|
||||
<v-btn class="ml-10 mb-5" color="primary" @click="goToDebugRoute">Debugger</v-btn>
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-label class="title-style-2">Jeu</v-label>
|
||||
<div class="mutltiple-per-line">
|
||||
<v-switch hide-details label="Jouer le son de succès lorsque des points sont ajoutés" v-model="successPlay" class="ml-15" color="primary" />
|
||||
</div>
|
||||
|
||||
<v-switch hide-details label="Jouer le son d'erreur lorsque des points sont enlevés" v-model="errorPlay" class="ml-15" color="primary" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
// Déclaration des variables réactives
|
||||
const embeddedSound = ref(false);
|
||||
const embeddedSoundVolume = ref(50);
|
||||
const mqttSound = ref(false);
|
||||
const mqttBrokerState = ref(false);
|
||||
const satellitesDisplay = ref(false);
|
||||
const successPlay = ref(false);
|
||||
const errorPlay = ref(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const goToDebugRoute = () => {
|
||||
router.push({ name: 'Debugger MQTT' });
|
||||
};
|
||||
|
||||
// Synchronisation avec le localStorage au montage
|
||||
onMounted(() => {
|
||||
embeddedSound.value = localStorage.getItem('EmbeddedSound') === 'true' || false;
|
||||
mqttSound.value = localStorage.getItem('MQTTSound') === 'true' || false;
|
||||
embeddedSoundVolume.value = Number(localStorage.getItem('EmbeddedSoundVolume')) || 50;
|
||||
satellitesDisplay.value = localStorage.getItem('SattelitesDisplay') === 'true' || false;
|
||||
successPlay.value = localStorage.getItem('SuccessPlay') === 'true' || false;
|
||||
errorPlay.value = localStorage.getItem('ErrorPlay') === 'true' || false;
|
||||
});
|
||||
|
||||
// Watchers pour mettre à jour localStorage automatiquement
|
||||
watch(embeddedSound, (val) => {
|
||||
localStorage.setItem('EmbeddedSound', val);
|
||||
});
|
||||
watch(embeddedSoundVolume, (val) => {
|
||||
localStorage.setItem('EmbeddedSoundVolume', val);
|
||||
});
|
||||
watch(mqttSound, (val) => {
|
||||
localStorage.setItem('MQTTSound', val);
|
||||
});
|
||||
watch(satellitesDisplay, (val) => {
|
||||
localStorage.setItem('SattelitesDisplay', val);
|
||||
});
|
||||
watch(successPlay, (val) => {
|
||||
localStorage.setItem('SuccessPlay', val);
|
||||
});
|
||||
watch(errorPlay, (val) => {
|
||||
localStorage.setItem('ErrorPlay', val);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title-style-1 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 16px;
|
||||
margin-left: 20px;
|
||||
font-size: 30px;
|
||||
opacity: 100%;
|
||||
font-weight: 500;
|
||||
}
|
||||
.title-style-2 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 40px;
|
||||
font-size: 25px;
|
||||
opacity: 100%;
|
||||
font-weight: 500;
|
||||
}
|
||||
.mutltiple-per-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.v-slider-style {
|
||||
width: 250px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||