Merge branch 'quentin' of gitlab.enst.fr:pact/2022-2023/pact71 into main

This commit is contained in:
Quentin Roussel
2023-03-20 11:57:54 +01:00
106 changed files with 4963 additions and 3261 deletions

30
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"configurations": [
{
"name": "Docker Node.js Launch",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"platform": "node",
"node": {
"package": "${workspaceFolder}/code/reviews_api/package.json",
"localRoot": "${workspaceFolder}/code/reviews_api"
}
},
{
"name": "Docker: Python - General",
"type": "docker",
"request": "launch",
"preLaunchTask": "docker-run: debug",
"python": {
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app"
}
],
"projectType": "general"
}
}
]
}

46
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,46 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "docker-build",
"label": "docker-build",
"platform": "node",
"dockerBuild": {
"dockerfile": "${workspaceFolder}/code/reviews_api/Dockerfile",
"context": "${workspaceFolder}/code/reviews_api",
"pull": true
},
"node": {
"package": "${workspaceFolder}/code/reviews_api/package.json"
}
},
{
"type": "docker-run",
"label": "docker-run: release",
"dependsOn": [
"docker-build"
],
"platform": "node",
"node": {
"package": "${workspaceFolder}/code/reviews_api/package.json"
}
},
{
"type": "docker-run",
"label": "docker-run: debug",
"dependsOn": [
"docker-build"
],
"dockerRun": {
"env": {
"DEBUG": "*",
"NODE_ENV": "development"
}
},
"node": {
"package": "${workspaceFolder}/code/reviews_api/package.json",
"enableDebugging": true
}
}
]
}

View File

@@ -1,23 +0,0 @@
Ceci est votre dépôt pour le projet PACT.
Vous **DEVEZ** modifier ce fichier (`README.md`) et créer tous les
répertoires et fichiers dont vous aurez besoin pour votre projet.
# Important, le rapport d'avancement
Le répertoire `rapport` contient un squelette pour votre rapporte d'avancement.
Ce répertoire **ne doit pas être renommé** ni le fichier `README.adoc` qu'il contient.
Le fichier `README.adoc` est le point d'entrée du rapport.
Il est rédigé en utilisant le langage [**AsciiDoc**](http://asciidoc.org/).
La syntaxe est supportée par GitLab qui le formatera correctement dans l'interface Web.
Le document final sera généré en utilisant l'outil [Asciidoctor](http://asciidoctor.org/) qui supporte les mêmes extensions que GitLab (pour les équations par exemple).
Un résumé de la syntaxe supportée est accessible [ici](http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/).
Vous pouvez éditer les différents fichiers en utilisant *votre éditeur de texte favori*.
Si vous n'en avez pas, vous pouvez, par exemple utiliser:
- [**Visual Studio Code**](https://code.visualstudio.com/) avec l'extension [AsciiDoc](https://marketplace.visualstudio.com/items?itemName=asciidoctor.asciidoctor-vscode) qui ajoute coloration syntaxique et rendu en temps réel.
- Ou bien sûr votre éditeur de text préféré **Sublim Text**, **Vim**, **Emacs**,…
- Une extension pour navigateur Web est aussi disponible pour visualiser le résultat ([**ici**](https://github.com/asciidoctor/asciidoctor-browser-extension)).

View File

@@ -0,0 +1,27 @@
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View File

@@ -0,0 +1,20 @@
FROM python:3.8-slim
#Ne pas créer les fichiers .pyc
ENV PYTHONDONTWRITEBYTECODE=1
#Afficher les logs directement dans le terminal
ENV PYTHONUNBUFFERED=1
#Installation des dépendances de opencv
RUN apt-get update
RUN apt-get install ffmpeg libsm6 libxext6 -y
# Installation des dépendances python
COPY requirements.txt .
RUN python -m pip install -r requirements.txt
# Création du répertoire de travail
WORKDIR /app
COPY . /app
CMD ["python", "main.py"]

View File

@@ -10,7 +10,7 @@ avis = {
"commentaire": "Commentaire"
}
# res = requests.post("http://localhost:8080/add_review", data=avis)
res = requests.post("http://localhost:8080/add_review", data=avis)
# print(res.text)
#Exemple ajout d'un commentaire trouvé sur les réseaux sociaux
@@ -25,5 +25,5 @@ avis = {
"auteur_lien": "https://instagram.com/michel",
}
res = requests.post("http://localhost:8080/add_social_review", data=avis)
# res = requests.post("http://localhost:8080/add_social_review", data=avis)
print(res.text)

View File

@@ -0,0 +1,44 @@
import cv2
import mediapipe as mp
mp_drawing = mp.solutions.drawing_utils
mp_drawing_styles = mp.solutions.drawing_styles
mp_hands = mp.solutions.hands
# For webcam input:
cap = cv2.VideoCapture(0)
hands = mp_hands.Hands(
model_complexity=0,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
def frame():
if cap.isOpened():
success, image = cap.read()
if not success:
print("Ignoring empty camera frame.")
# If loading a video, use 'break' instead of 'continue'.
return
# To improve performance, optionally mark the image as not writeable to
# pass by reference.
image.flags.writeable = False
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
results = hands.process(image)
# Draw the hand annotations on the image.
image.flags.writeable = True
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
mp_drawing.draw_landmarks(
image,
hand_landmarks,
mp_hands.HAND_CONNECTIONS,
mp_drawing_styles.get_default_hand_landmarks_style(),
mp_drawing_styles.get_default_hand_connections_style())
# Flip the image horizontally for a selfie-view display.
# cv2.imshow('MediaPipe Hands', cv2.flip(image, 1))
if cv2.waitKey(5) & 0xFF == 27:
return
# cap.release()

View File

@@ -0,0 +1,39 @@
import asyncio
import json
import math
import websockets
import random
import os
import hands
import time
values = []
class WebsocketServer:
def __init__(self,getEffects,port=os.getenv("PORT"),host=os.getenv("HOST")) -> None:
self.host = host
self.port = port
self.getEffects = getEffects
async def run(self):
async with websockets.serve(self.handler, self.host, self.port):
await asyncio.Future()
async def handler(self,websocket):
while True:
start = time.time()
messages = self.getEffects()
hands.frame()
await websocket.send(json.dumps(messages))
# await asyncio.sleep(1/30)
delay = time.time() - start
values.append(1/delay)
avg = sum(values) / len(values)
dev = [(v - avg) ** 2 for v in values]
print(avg, math.sqrt(sum(dev)/len(dev)))
#Remplacer ça par la fonction qui récupère les effets (dans le module de reconnaissance de gestes)
def getEffects():
return {"type": "effects", "effects": [{"type": "thumbs_up", "x":random.randint(0,100), "y": random.randint(0,100), "width": 50, "height": 50}]}
server = WebsocketServer(getEffects)
asyncio.run(server.run())

View File

@@ -0,0 +1,4 @@
websockets
requests
opencv-python
mediapipe

View File

@@ -1,27 +1,12 @@
-- phpMyAdmin SQL Dump
-- version 4.9.5deb2
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Dec 26, 2022 at 10:31 AM
-- Server version: 8.0.31-0ubuntu0.20.04.1
-- PHP Version: 7.4.3
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `telereview`
--
CREATE DATABASE IF NOT EXISTS `telereview` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE DATABASE IF NOT EXISTS `telereview`;
USE `telereview`;
-- --------------------------------------------------------
@@ -32,9 +17,9 @@ USE `telereview`;
CREATE TABLE `borne_auteurs` (
`id` int NOT NULL,
`sexe` tinytext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`sexe` tinytext ,
`age` tinyint DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -49,7 +34,7 @@ CREATE TABLE `borne_avis` (
`note_principale` tinyint NOT NULL,
`commentaire` text NOT NULL,
`source_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -60,7 +45,7 @@ CREATE TABLE `borne_avis` (
CREATE TABLE `borne_criteres` (
`id` int NOT NULL,
`nom` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
--
-- Dumping data for table `borne_criteres`
@@ -83,7 +68,7 @@ CREATE TABLE `borne_notes_autre` (
`critere_id` int NOT NULL,
`avis_id` int NOT NULL,
`note` int NOT NULL COMMENT 'Note sur 10'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -96,7 +81,7 @@ CREATE TABLE `reseaux_sociaux_auteurs` (
`nom_utilisateur` text NOT NULL,
`source_id` int NOT NULL,
`lien` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -109,10 +94,10 @@ CREATE TABLE `reseaux_sociaux_avis` (
`date` date NOT NULL,
`source_id` int NOT NULL,
`note` tinyint DEFAULT NULL,
`commentaire` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`commentaire` text ,
`auteur_id` int NOT NULL,
`lien_source` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
`lien_source` text
) ;
-- --------------------------------------------------------
@@ -123,7 +108,7 @@ CREATE TABLE `reseaux_sociaux_avis` (
CREATE TABLE `sources` (
`id` int NOT NULL,
`nom` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
--
-- Dumping data for table `sources`
@@ -145,7 +130,7 @@ CREATE TABLE `stats_autres_annee` (
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`critere_id` int NOT NULL,
`note` float NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -158,7 +143,7 @@ CREATE TABLE `stats_autres_jour` (
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`critere_id` int NOT NULL,
`note` float NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -171,7 +156,7 @@ CREATE TABLE `stats_autres_mois` (
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`critere_id` int NOT NULL,
`note` float NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -184,7 +169,7 @@ CREATE TABLE `stats_autres_semaine` (
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`critere_id` int NOT NULL,
`note` float NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -200,7 +185,7 @@ CREATE TABLE `stats_general_annee` (
`moyenne_borne` float NOT NULL,
`dist_age` text NOT NULL COMMENT 'Distribution de l''age des auteurs',
`dist_sexe` text NOT NULL COMMENT 'Distribution du sexe des auteurs'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -214,9 +199,9 @@ CREATE TABLE `stats_general_jour` (
`moyenne_globale` float DEFAULT NULL,
`moyenne_site` float DEFAULT NULL,
`moyenne_borne` float DEFAULT NULL,
`dist_age` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT 'Distribution de l''age des auteurs',
`dist_sexe` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT 'Distribution du sexe des auteurs'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
`dist_age` text COMMENT 'Distribution de l''age des auteurs',
`dist_sexe` text COMMENT 'Distribution du sexe des auteurs'
) ;
-- --------------------------------------------------------
@@ -232,7 +217,7 @@ CREATE TABLE `stats_general_mois` (
`moyenne_borne` float NOT NULL,
`dist_age` text NOT NULL COMMENT 'Distribution de l''age des auteurs',
`dist_sexe` text NOT NULL COMMENT 'Distribution du sexe des auteurs'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
-- --------------------------------------------------------
@@ -248,7 +233,7 @@ CREATE TABLE `stats_general_semaine` (
`moyenne_borne` float NOT NULL,
`dist_age` text NOT NULL COMMENT 'Distribution de l''age des auteurs',
`dist_sexe` text NOT NULL COMMENT 'Distribution du sexe des auteurs'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
) ;
--
-- Indexes for dumped tables
@@ -438,7 +423,3 @@ ALTER TABLE `stats_general_mois`
ALTER TABLE `stats_general_semaine`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

98
code/docker-compose.yaml Normal file
View File

@@ -0,0 +1,98 @@
version: "3.9"
services:
#Base de donnée mysql de la borne sur laquelle est stockée tous les avis et les stats
db:
image: mysql:latest
container_name: db
expose:
- 3306
volumes:
- ./db:/docker-entrypoint-initdb.d
restart: always
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-uroot"] # Command to check health.
interval: 5s # Interval between health checks.
timeout: 5s # Timeout for each health checking.
retries: 20 # Hou many times retries.
start_period: 10s # Estimated time to boot.
environment:
MYSQL_ROOT_PASSWORD: telereview
MYSQL_DATABASE: telereview
#Interface d'aministration pour la bdd
phpmyadmin:
image: phpmyadmin:latest
restart: always
container_name: phpmyadmin
depends_on:
db:
condition: service_healthy
environment:
PMA_ARBITRARY: 1
PMA_HOST: db
PMA_USER: root
PMA_PASSWORD: telereview
ports:
- 8000:80
#API de gestion des avis, permet d'ajouter ou de récuperer des avis ou les stats sur les avis par des requêtes HTTP
reviews_api:
container_name: reviews_api
ports:
- 8080:8080
environment:
- NODE_ENV=production
- DB_USER=root
- DB_PASSWORD=telereview
- DB_HOST=db
- DB_NAME=telereview
- PORT=8080
depends_on:
db:
condition: service_healthy
build: ./reviews_api
restart: always
#Serveur web de l'interface de la borne
interface_borne:
image: httpd:latest
volumes:
- ./interface_borne:/usr/local/apache2/htdocs/
container_name: interface_borne
ports:
- 8888:80
#Serveur web de l'interface admin
interface_admin:
image: httpd:latest
volumes:
- ./interface_admin:/usr/local/apache2/htdocs/
container_name: interface_admin
ports:
- 80:80
#Backend de la borne : scripts pythons de reconnaissances video et audio
#Envoient les infos a l'interface de la borne par websocket pour mettre a jour l'interface rapidement
#Met a jour les avis en faisant des requêtes a l'API
backend_reconnaissance:
build: ./backend_reconnaissance
container_name: backend_reconnaissance
restart: always
devices:
- /dev/video3:/dev/video0
environment:
- PORT=5000
- HOST=backend_reconnaissance
ports:
#Ce container est le serveur websocker dont le client est l'interface de la borne qui tourne dans le navigateur
- 5000:5000
video_loopback:
build: ./video_loopback
container_name: video_loopback
restart: always
devices:
- /dev/video0:/dev/video0
- /dev/video2:/dev/video1
- /dev/video3:/dev/video2

2
code/interface_admin/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.next
package-lock.json

View File

@@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -0,0 +1,58 @@
import React from 'react'
import { Card, Col, Row, Table } from 'react-bootstrap';
import { BsPersonFill } from 'react-icons/bs';
import styles from '../styles/Avis.module.css'
export default function Avis({review}) {
const {date, note_principale,notes_autres, commentaire, sexe_auteur, nom_source, age_auteur} = review;
return (
<Card>
<Card.Title>Avis</Card.Title>
<Card.Body>
<Row>
<h2>Auteur</h2>
<Col xs={1}>
<BsPersonFill className={styles.personIcon} />
</Col>
<Col className='d-flex flex-column'>
<p>Age : {age_auteur}</p>
<p>Sexe : {sexe_auteur}</p>
<p>Date de publication : {date}</p>
<p>Source : {nom_source}</p>
</Col>
</Row>
<Row>
<h2>Notes</h2>
<Table>
<thead>
<tr>
<th>Critère</th>
<th>Note</th>
</tr>
</thead>
<tbody>
<tr>
<td>Général</td>
<td>{note_principale} / 10</td>
</tr>
{notes_autres && notes_autres.map(({ critere, note }) => {
return <tr key={critere}>
<td>{critere}</td>
<td>{note}/10</td>
</tr>
})}
</tbody>
</Table>
</Row>
<Row>
<Card>
<Card.Header>Commentaire</Card.Header>
<Card.Body>
{commentaire}
</Card.Body>
</Card>
</Row>
</Card.Body>
</Card>
)
}

View File

@@ -0,0 +1,34 @@
import { useRouter } from 'next/router';
import React from 'react'
import { Table } from 'react-bootstrap'
import styles from '../styles/AvisList.module.css'
export default function AvisList({ avis }) {
const router = useRouter();
function handleClick(id) {
router.push(`/avis/${id}`);
}
return (
<Table>
<thead>
<tr>
<th>Date</th>
<th>Note globale</th>
<th>Commentaire</th>
<th>Source</th>
</tr>
</thead>
<tbody>
{avis.map(({ id, note_principale, commentaire, date, nom_source }) => {
return <tr onClick={() => handleClick(id)} key={id} className={styles.row}>
<td>{date}</td>
<td>{note_principale} / 10</td>
<td>{commentaire}</td>
<td>{nom_source}</td>
</tr>
})}
</tbody>
</Table>
)
}

View File

@@ -0,0 +1,47 @@
import React from 'react'
import { Bar } from 'react-chartjs-2'
import Chart from 'chart.js/auto';
export default function ComparativeBarChart({ xlabels, data0, label0, data1, label1}) {
return (
<Bar
options={{
// plugins: {
// title: {
// display: true,
// text: title
// },
// },
responsive: true,
interaction: {
intersect: false,
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true
}
}
}}
data={{
labels: xlabels,
datasets: [
{
label: label0,
data: data0,
backgroundColor: "#FF3B30",
stack: "stack0"
},
{
label: label1,
data: data1,
backgroundColor: "#0000FF",
stack: "stack1"
}
]
}}
/>
)
}

View File

@@ -0,0 +1,23 @@
import React from 'react'
import Link from 'next/link'
import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
export default function Menu() {
return (
<Navbar bg="light" expand="lg">
<Container>
<Navbar.Brand href="#home">Téléreview</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="me-auto">
<Link href="/" passHref legacyBehavior><Nav.Link>Accueil</Nav.Link></Link>
<Link href="/stats" passHref legacyBehavior><Nav.Link>Statistiques</Nav.Link></Link>
<Link href="/avis" passHref legacyBehavior><Nav.Link>Avis</Nav.Link></Link>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
)
}

View File

@@ -0,0 +1,3 @@
export const api = {
HOST: 'localhost:8080'
}

View File

@@ -0,0 +1,32 @@
import { useEffect, useState } from "react";
import { api } from "../config/reviewsApi";
function useReview(reviewId) {
const [review, setReview] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
async function fetchData(id) {
const response = await fetch('http://' + api.HOST + `/borne/get_review?id=${id}`)
if (response.ok) {
const jsonData = await response.json();
setReview(jsonData);
setLoading(false);
setError(false);
} else {
setError(true);
setLoading(false);
}
}
useEffect(() => {
if (reviewId) {
fetchData(reviewId);
}
}, [reviewId])
return { review, loading, error }
}
export default useReview;

View File

@@ -0,0 +1,30 @@
import { useEffect, useState } from "react";
import { api } from "../config/reviewsApi";
export default function useReviews() {
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
async function fetchLastReviews(limit=100) {
setLoading(true);
const response = await fetch('http://' + api.HOST + '/borne/get_last_reviews', {
method: 'GET'
})
if(response.ok) {
let json = await response.json()
setReviews(json);
setError(false);
setLoading(false);
}else {
setLoading(false);
setError(true);
}
}
useEffect(() => {
fetchLastReviews();
}, [])
return {reviews, error, loading, fetchLastReviews};
}

View File

@@ -0,0 +1,26 @@
import { useEffect } from "react";
import { api } from "../config/reviewsApi";
export default function useStats(limit, interval) {
const [stats, setStats] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
async function fetchData(limit, interval) {
const response = await fetch("http://" + api.HOST + `/get_stats?interval=${interval}&limit=${limit}`)
if(response.ok) {
const data = await response.json();
setStats(data);
setError(false);
}else {
setError(true)
}
setLoading(false);
}
useEffect(() => {
fetchData(limit, interval);
})
return {stats, loading, error};
}

View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

View File

@@ -0,0 +1,24 @@
{
"name": "interface-admin",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@next/font": "13.1.6",
"bootstrap": "^5.2.3",
"chart.js": "^4.2.0",
"eslint": "8.33.0",
"eslint-config-next": "13.1.6",
"next": "13.1.6",
"react": "18.2.0",
"react-bootstrap": "^2.7.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.7.1"
}
}

View File

@@ -0,0 +1,13 @@
import Menu from '../components/Menu'
import '../styles/globals.css'
import 'bootstrap/dist/css/bootstrap.css';
import { Container } from 'react-bootstrap';
export default function App({ Component, pageProps }) {
return <>
<Menu />
<Container fluid="md">
<Component {...pageProps} />
</Container>
</>
}

View File

@@ -0,0 +1,16 @@
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html lang="en">
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}

View File

@@ -0,0 +1,12 @@
import { useRouter } from 'next/router';
import React from 'react'
import Avis from '../../components/Avis';
import useReview from '../../hooks/review';
export default function AvisPage() {
const router = useRouter();
const {id} = router.query;
const {review, loading, error} = useReview(id);
return (
!loading && !error && <Avis review={review}/>
)
}

View File

@@ -0,0 +1,75 @@
import React, { useEffect, useState } from 'react'
import { Card, Container, Form, Row, Table } from 'react-bootstrap'
import AvisList from '../../components/AvisList';
import useReviews from '../../hooks/reviews';
import styles from '../../styles/AvisListPage.module.css'
export default function AvisListPage() {
const [minGrade, setMinGrade] = useState(0);
const [maxGrade, setMaxGrade] = useState(10);
const [sources, setSources] = useState({'borne': true, 'website': true})
const [filteredReviews, setFilteredREviews] = useState([])
const {reviews, error, loading} = useReviews();
useEffect(() => {
console.log(reviews)
const newReviews = reviews.filter((review) => review.note_principale >= minGrade && review.note_principale <= maxGrade && sources[review.nom_source])
setFilteredREviews(newReviews)
}, [reviews, minGrade, maxGrade, sources])
useEffect(() => {
if(minGrade > maxGrade) {
setMinGrade(maxGrade);
}
}, [maxGrade]);
useEffect(() => {
if(minGrade > maxGrade) {
setMaxGrade(minGrade);
}
}, [minGrade])
return (
<Container fluid>
<Card>
<Card.Header>Tous les avis</Card.Header>
<Card.Body>
<Row>
<Form>
<Form.Group>
<Form.Label>Types d'avis</Form.Label>
<Form.Check
type="switch"
label="Borne"
onChange={(e) => setSources({...sources, 'borne': e.target.checked})}
checked={sources['borne']}
/>
<Form.Check
type="switch"
label="QR Code"
onChange={(e) => setSources({...sources, 'website': e.target.checked})}
checked={sources['website']}
/>
</Form.Group>
<Form.Group>
<Form.Label>Note</Form.Label>
<div className='d-flex flex-row justify-content-around col-md-6'>
<div>Min : {minGrade}/10</div>
<div className={styles.sliderContainer}>
<input type="range" value={minGrade} onChange={(e) => setMinGrade(e.target.value)} min="0" max="10" step="1" className={styles.slider}></input>
<input type="range" value={maxGrade} onChange={(e) => setMaxGrade(e.target.value)} min="0" max="10" step="1" className={styles.slider}></input>
</div>
<div>Max : {maxGrade}/10</div>
</div>
</Form.Group>
</Form>
</Row>
<Row>
{!loading && !error && <AvisList avis={filteredReviews} />}
</Row>
</Card.Body>
</Card>
</Container >
)
}

View File

@@ -0,0 +1,76 @@
import Head from 'next/head'
import { Card, Container } from 'react-bootstrap'
import ComparativeBarChart from '../components/ComparativeBarChart'
import { useEffect, useState } from 'react'
import styles from "../styles/Home.module.css"
export default function Home() {
const [datasets, setDatasets] = useState(null);
const [averages, setAverages] = useState(null);
const [differences, setDifferences] = useState(null);
useEffect(() => {
if (datasets) {
let newAverages = []
let newDifferences = []
for (let i = 0; i < datasets.length; i++) {
newAverages[i] = datasets[i].current.reduce((a, b) => a + b) / datasets[i].current.length
newDifferences[i] = newAverages[i] - datasets[i].previous.reduce((a, b) => a + b) / datasets[i].previous.length
}
setAverages(newAverages);
setDifferences(newDifferences);
}
}, [datasets]);
useEffect(() => setDatasets([
{ title: "Nombre d'avis", current: [3, 2, 3, 4, 5, 6, 7], previous: [7, 6, 5, 4, 3, 2, 1] },
{ title: "Notes moyennes", current: [1, 2, 3, 4, 5, 6, 7], previous: [7, 6, 5, 4, 3, 2, 1] }
]), []);
function dataVisualizer(title, current, previous, average, difference) {
return <div key={title}>
<h3>{title}</h3>
<Card className={styles.averageCard}>
<Card.Title>Moyenne</Card.Title>
<Card.Body className={styles.averageCardBody}>
<div
className={styles.averageMainValue}
>
{Math.round(average * 1e2) / 1e2}
</div>
<div
className={[styles.averageCardSecondaryValue, difference >= 0 ? styles.averagePositive : styles.averageNegative].join(' ')}
>
{(difference >= 0 ? "+" : "-") + Math.round(difference * 1e2) / 1e2}
</div>
</Card.Body>
</Card>
<ComparativeBarChart
xlabels={["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]}
label0="Cette semaine"
label1="La semaine dernière"
data0={current}
data1={previous}
/>
<hr />
</div>
}
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Page d'accueil" />
</Head>
<Container fluid>
<Card>
<Card.Header as="h2">Vos performances cette semaine</Card.Header>
<Card.Body>
{datasets && averages && differences && datasets.map((set, i) => dataVisualizer(set.title, set.current, set.previous, averages[i], differences[i]))}
</Card.Body>
<div className='col col-12 col-lg-8 mx-auto'>
</div>
</Card>
</Container>
</>
)
}

View File

@@ -0,0 +1,7 @@
import React from 'react'
export default function Stats() {
return (
<div>stats</div>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,5 @@
.personIcon {
width: 100%;
height: 100%;
/* font-size: 50px; */
}

View File

@@ -0,0 +1,6 @@
/* ==== TABLEAU ==== */
.row:hover {
cursor: pointer;
background-color: #EEE;
}

View File

@@ -0,0 +1,57 @@
/* ==== SLIDER ==== */
.sliderContainer {
position: relative;
width: 300px;
}
.sliderContainer > input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
pointer-events: all;
width: 24px;
height: 24px;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 0 0 1px #C6C6C6;
cursor: pointer;
z-index: 99;
}
.sliderContainer > input[type=range]::-moz-range-thumb {
z-index: 99;
pointer-events: all;
width: 24px;
height: 24px;
background-color: #fff;
border-radius: 50%;
box-shadow: 0 0 0 1px #C6C6C6;
cursor: pointer;
}
.sliderContainer > input[type=range]::-webkit-slider-thumb:hover {
background: #f7f7f7;
}
.sliderContainer >input[type=range]::-webkit-slider-thumb:active {
box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
-webkit-box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
}
.sliderContainer > input[type=range]::-moz-range-thumb:hover {
background: #f7f7f7;
}
.sliderContainer >input[type=range]::-moz-range-thumb:active {
box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
-webkit-box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
}
.sliderContainer >input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 2px;
width: 100%;
position: absolute;
background-color: #C6C6C6;
pointer-events: none;
}

View File

@@ -0,0 +1,29 @@
.averageCard {
width: min-content;
margin: 0 auto;
padding: 10px;
text-align: center;
min-width: 300px;
}
.averageCardBody {
display: flex;
align-items: center;
margin: 0 auto;
}
.averageMainValue {
font-size: 40px;
}
.averageSecondaryValue {
font-size: 20px;
}
.averagePositive {
color: green;
}
.averageNegative {
color: red;
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
html, body {
margin: 0;
height: 100%;
}
.page {
width: 100%;
height: 100%;
}
#camera > video, #camera > canvas {
position: absolute;
top: 0;
left: 0;
text-align: center;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
}
#camera > video {
z-index: 0;
}
#camera > canvas {
z-index: 1;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@@ -0,0 +1,11 @@
class AudioPage {
constructor() {
this.isEnabled = false;
this.DOMElement = document.getElementById("audio");
}
set enabled(isEnabled) {
this.isEnabled = isEnabled;
this.DOMElement.style.display = isEnabled ? "block" : "none";
}
}

View File

@@ -0,0 +1,145 @@
class CameraPage {
constructor() {
this.spinnerWeight = 10;
this.spinnerColor = "#0F0FFF"
this.canvas = document.getElementById("overlay-canvas");
this.ctx = this.canvas.getContext("2d");
this.video = document.getElementById("camera-video");
this.width;
this.height; //calcule automatiquement en fonction de la largeur du flux vidéo
this.videoWidth;
this.videoHeight;
this.streaming = false;
this.activeEffects = [];
this.images = {};
this._startup();
this._loadImages();
this._enabled = false;
this.DOMElement = document.getElementById("camera");
}
set enabled(val) {
this._enabled = val;
this.DOMElement.style.display = val ? "block" : "none";
if (val) {
this._frame();
this.video.play();
}else {
this.video.pause();
}
}
get enabled() {
return this._enabled;
}
_startup() {
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((stream) => {
this.video.srcObject = stream;
this.video.play();
})
.catch((err) => {
console.error(`Erreur pendant la lecture de la camera: ${err}`);
});
this.video.addEventListener(
"canplay",
(ev) => {
if (!this.streaming) {
//calcul de la taille de la vidéo en fonction de la taille de la fenêtre pour qu'elle soit toujours visible
let aspectRatio = this.video.videoWidth / this.video.videoHeight;
if (window.innerHeight * aspectRatio > window.innerWidth) {
this.width = window.innerWidth;
this.height = window.innerWidth / aspectRatio;
} else {
this.width = window.innerHeight * aspectRatio;
this.height = window.innerHeight;
}
this.videoHeight = this.video.videoHeight;
this.videoWidth = this.video.videoWidth;
this.video.setAttribute("width", this.width);
this.video.setAttribute("height", this.height);
this.canvas.setAttribute("width", this.width);
this.canvas.setAttribute("height", this.height);
this.streaming = true;
}
},
false
);
}
_loadImages() {
this.images.thumbsUp = new Image();
this.images.thumbsUp.src = "assets/img/thumbs_up.png";
this.images.thumbsDown = new Image();
this.images.thumbsDown.src = "assets/img/thumbs_down.png";
}
_frame() {
if (this.streaming && this.enabled && this.width && this.height) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// this.ctx.drawImage(this.video, 0, 0, this.width, this.height);
this._drawEffects();
}
if (this.enabled) {
requestAnimationFrame(() => this._frame());
}
}
_scaleEffect(x, y, width, height) {
let xScale = this.width / this.videoWidth;
let yScale = this.height / this.videoHeight;
return {
x: x * xScale,
y: y * yScale,
width: width * xScale,
height: height * yScale
}
}
_drawEffects() {
for (let effect of this.activeEffects) {
let { x, y, width, height } = this._scaleEffect(effect.x, effect.y, effect.width, effect.height);
if (effect.type == "thumbs_down") {
this._drawThumbsDown(x, y, width, height);
}
if (effect.type == "thumbs_up") {
this._drawThumbsUp(x, y, width, height);
}
if (effect.type == "loading") {
this._drawLoading(x, y, width, effect.progress);
}
}
}
_drawLoading(x, y, radius, progress) {
this.ctx.lineWidth = this.spinnerWeight;
this.ctx.strokeStyle = this.spinnerColor;
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, progress * 2 * Math.PI);
this.ctx.stroke();
}
_drawThumbsDown(x, y, width, height) {
this.ctx.beginPath();
this.ctx.drawImage(this.images.thumbsDown, x, y, width, height);
this.ctx.stroke();
}
_drawThumbsUp(x, y, width, height) {
this.ctx.beginPath();
this.ctx.drawImage(this.images.thumbsUp, x, y, width, height);
this.ctx.stroke();
}
setEffects(effects) {
this.activeEffects = effects;
}
}

View File

@@ -0,0 +1,5 @@
let stateManager;
window.addEventListener("load", () => {
stateManager = new StateManager();
}, false);

View File

@@ -0,0 +1,16 @@
class WebsocketClient {
constructor(onNewEffects, onNewState) {
this.socket = new WebSocket("ws://localhost:5000");
this.socket.addEventListener("open", (event) => {
this.socket.send("connected");
});
this.socket.addEventListener("message", (event) => {
let msg = JSON.parse(event.data);
if (msg.type == "effects") {
onNewEffects(msg.effects);
}else if(msg.type == "state") {
onNewState(msg.state);
}
});
}
}

View File

@@ -0,0 +1,12 @@
class SleepingPage {
constructor(onWakeUp) {
this.onWakeUp = onWakeUp;
this.isEnabled = false;
this.DOMElement = document.getElementById("sleeping-page");
}
set enabled(isEnabled) {
this.isEnabled = isEnabled;
this.DOMElement.style.display = isEnabled ? "block" : "none";
}
}

View File

@@ -0,0 +1,49 @@
const STATE = {
sleeping: 0,
video: 1,
audio: 2,
thankYou: 3,
};
class StateManager {
constructor() {
this._state = STATE.sleeping;
this._cameraPage = new CameraPage();
this._sleepingPage = new SleepingPage();
this._audioPage = new AudioPage();
this._thankYouPage = new ThankYouPage();
this.wsClient = new WebsocketClient(
(effects) => this._cameraPage.setEffects(effects),
(state) => this.changeState(state)
);
this._sleepingPage.enabled = true;
this._cameraPage.enabled = false;
this._audioPage.enabled = false;
this._thankYouPage.enabled = false;
//TODO: Remove qd implémenté dans le backend
document.getElementById("sleeping-page-continue").onclick = () => this.setState(STATE.video);
}
setState(newState) {
console.log({current:this._state,new:newState})
if(this._state == STATE.sleeping && newState == STATE.video) {
this._cameraPage.enabled = true;
this._sleepingPage.enabled = false;
this._state = newState;
}else if(this._state == STATE.video && newState == STATE.audio) {
this._cameraPage.enabled = false;
this._audioPage.enabled = true;
this._state = newState;
}else if(this._state == STATE.audio && newState == STATE.thankYou) {
this._audioPage.enabled = false;
this._thankYouPage.enabled = true;
this._state = newState;
}else if(this._state == STATE.thankYou && newState == STATE.sleeping) {
this._thankYouPage.enabled = false;
this._sleepingPage.enabled = true;
this._state = newState;
}
}
}

View File

@@ -0,0 +1,11 @@
class ThankYouPage {
constructor() {
this.isEnabled = false;
this.DOMElement = document.getElementById("thank-you");
}
set enabled(isEnabled) {
this.isEnabled = isEnabled;
this.DOMElement.style.display = isEnabled ? "block" : "none";
}
}

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="assets/css/main.css">
<!-- <link rel="stylesheet" href="assets/css/bootstrap-grid.min.css"> -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
<title>Téléreview</title>
</head>
<body>
<div id="sleeping-page" class="page">
<h1>Venez donner votre avis</h1>
<!-- Boutton pour test -->
<button id="sleeping-page-continue">Continuer</button>
</div>
<div id="camera">
<canvas id="overlay-canvas"></canvas>
<video id="camera-video"></video>
</div>
<div id="audio">
<h1>Enregistrement audio blabal</h1>
</div>
<div id="thank-you">
<h1>Merci pour votre avis !!!!!!!!!!</h1>
</div>
<script src="assets/js/camera_page.js"></script>
<script src="assets/js/network.js"></script>
<script src="assets/js/thank_you_page.js"></script>
<script src="assets/js/audio_page.js"></script>
<script src="assets/js/sleeping_page.js"></script>
<script src="assets/js/state_manager.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View File

@@ -0,0 +1,15 @@
FROM node:lts-alpine
WORKDIR /usr/src/app
#installation des dépendances
COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
RUN npm install --production --silent && mv node_modules ../
#On copie le code source
COPY . .
#On change le propriétaire du dossier
RUN chown -R node /usr/src/app
#On change l'utilisateur
USER node
CMD ["node", "index.js"]

View File

@@ -1,4 +1,7 @@
# Installation
# Serveur de traitement des données
Ce serveur s'occupe de fournir une API web pour pourvoir ajouter des avis sur la borne, réxupérer des avis, calculer et fournir les statistiques sur ces avis
# Installation (Si vous voulez le lancer hors du container docker)
* Pour faire fonctinoner le serveur sur vos machines il y a 3 choses a faire
1. Installer node js : https://nodejs.org/en/download/
2. Ouvrir un terminal et aller dans ce dossier (code/server) et tapper `npm install` pour installer les pacakges nécessaires
@@ -13,6 +16,7 @@
- `/borne/get_criteres` : renvoie les criteres de notations valide pour les notes autres
- `/borne/notes_autres?critere=CRIT&limit=LIM` : renvoie les LIM dernières notes sur le critère CRIT
- `/borne/notes_autres?id=ID&limit=LIM` : renvoie toutes les notes spécifiques liées à l'avis ID
- `get_stats?interval=INTERVAL&limit=LIM`: interval est "jour", "mois", "annee", "semaine", l'interval de calcul de données demandé, LIM est le nombre des stats à récupérer
### Routes POST
- `/add_review` : Ajoute une review et un auteur, paramètres POST :

View File

@@ -29,7 +29,12 @@ const getLastReviews = (limit=10) => {
*/
const getReviewFromId = (id) => {
return new Promise((resolve, reject) => {
let sql = `SELECT * FROM borne_avis WHERE id = ? LIMIT 1`;
let sql = `SELECT borne_avis.id,date,note_principale,commentaire,sources.nom as nom_source, borne_auteurs.sexe as sexe_auteur, borne_auteurs.age as age_auteur
FROM borne_avis
JOIN sources ON sources.id = source_id
JOIN borne_auteurs ON borne_auteurs.id = id_auteur
WHERE borne_avis.id = ?
LIMIT 1`;
conn.query(sql, [id], (err, res) => {
if (err) {
reject(err);
@@ -150,7 +155,9 @@ export const handleGetLastReviews = (req, res) => {
export const handleGetReview = (req, res) => {
getReviewFromId(req.query.id)
.then((review) => {
res.send(review);
getNotesAutresFromReview(req.query.id).then((notesAutres) => {
res.send({ ...review, notes_autres: notesAutres });
})
})
.catch((err) => {
res.status(500).send("Error: " + err.message);

View File

@@ -5,10 +5,11 @@ import { addReviewFromRequest } from './borne/post_handler.js';
import { addSocialReviewFromRequest } from './reseaux_sociaux/post_handler.js';
import { startCronJobs } from './stats/update_stats.js';
import * as borneHandler from './borne/get_handler.js';
import cors from "cors";
const app = express();
app.use(bodyParser.urlencoded({extended:true}))
app.use(cors({origin:'*'}))
dotenv.config()
app.post('/add_review', (req,res) => addReviewFromRequest(req,res));
app.post('/add_social_review', (req,res) => addSocialReviewFromRequest(req,res));

View File

@@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.1",
"cors": "^2.8.5",
"cron": "^2.1.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
@@ -108,6 +109,18 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/cron": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cron/-/cron-2.1.0.tgz",
@@ -491,6 +504,14 @@
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
@@ -802,6 +823,15 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"requires": {
"object-assign": "^4",
"vary": "^1"
}
},
"cron": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cron/-/cron-2.1.0.tgz",
@@ -1101,6 +1131,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"object-inspect": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",

View File

@@ -11,6 +11,7 @@
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.1",
"cors": "^2.8.5",
"cron": "^2.1.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",

2
code/setup.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
sudo modprobe v4l2loopback devices=2

View File

@@ -0,0 +1,3 @@
FROM alpine:latest
RUN apk add --no-cache ffmpeg
CMD ["ffmpeg","-video_size","640x480","-f","video4linux2","-i","/dev/video0","-codec","copy","-f","v4l2","/dev/video1","-codec","copy","-f","v4l2","/dev/video2", "-loglevel","warning"]

12
docs/dupliquer_camera.md Normal file
View File

@@ -0,0 +1,12 @@
# Methode pour accéder au flux video a la fois depuis firefox et depuis opencv
* installer v4l2loopback :
* Télécharger : `git clone https://github.com/umlaeute/v4l2loopback.git`
* Installer avec : `make` puis `sudo make install`
* activer le module : `sudo modprobe v4l2loopback devices=2`
* Erreur possible : opperation not permitted : il faut désactiver secure boot
* OU `apt update && apt install v4l2loopback-dkms v4l2loopback-utils`
* [Innutile si container video_loopback present] Faire looper la camera /dev/video0 sur les autres
* installer ffmpeg : `sudo apt get install ffmpeg`
* activer le loopback : `ffmpeg -video_size 640x480 -f video4linux2 -i /dev/video0 -codec copy -f v4l2 /dev/video1 -codec copy -f v4l2 /dev/video2`
Maintenant on peut par exemple utiliser /dev/video2 sur firefox et /dev/video13sur opencv sans que cela ne pose de problème

16
docs/liste_pages_web.md Normal file
View File

@@ -0,0 +1,16 @@
Liste des pages web à concevoir dans tout le projet
* Interface de la borne
* Interface avec retour vidéo et effets quand un gest est détécté
* page de remerciment, avec indication que la personne peut parler pour développer son avis
* Page de remerciment
* Formulaire de retour d'avis
* Formulaire
* Page de remerciement
* Interface admin
* Page principale avec les stats principales
* Page liste des avis récents
* page stats détaillées sur la borne ou on peut changer l'intervalle de calcul des stats
* Page avec les derniers avis récup sur les réseaux sociaux
* Page avec les stats récup sur les réseaux sociaux

View File

@@ -0,0 +1,6 @@
Truc a corriger sur le rapport
* XRemplacer les screen par des vrai graphiques
* XAjouter des standard deviation aux graphiques
* XPeut être ajouter des valeurs absolues sur les benchmark
* X Regarder si utiliser un power saving mode sur le CPU est intéressant (température vs perofrmances)
* X Dire que c'est pas du temps mais que ça nous convient

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,5 @@
FROM alpine:latest
RUN apk add --no-cache sysbench
WORKDIR /app
COPY benchmark_script.sh /app/benchmark_script.sh
CMD ["sh","benchmark_script.sh"]

View File

@@ -0,0 +1,5 @@
sysbench --test=cpu run >>sysbench.log
sysbench --test=memory run >>sysbench.log
sysbench --test=fileio --file-test-mode=rndrw prepare
sysbench --test=fileio --file-test-mode=rndrw run >>sysbench.log
sysbench --test=fileio cleanup

View File

@@ -0,0 +1,114 @@
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Prime numbers limit: 10000
Initializing worker threads...
Threads started!
CPU speed:
events per second: 613.74
General statistics:
total time: 10.0013s
total number of events: 6140
Latency (ms):
min: 1.62
avg: 1.63
max: 2.10
95th percentile: 1.64
sum: 9998.37
Threads fairness:
events (avg/stddev): 6140.0000/0.00
execution time (avg/stddev): 9.9984/0.00
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Running memory speed test with the following options:
block size: 1KiB
total size: 102400MiB
operation: write
scope: global
Initializing worker threads...
Threads started!
Total operations: 33390517 (3338076.27 per second)
32607.93 MiB transferred (3259.84 MiB/sec)
General statistics:
total time: 10.0000s
total number of events: 33390517
Latency (ms):
min: 0.00
avg: 0.00
max: 0.10
95th percentile: 0.00
sum: 4949.30
Threads fairness:
events (avg/stddev): 33390517.0000/0.00
execution time (avg/stddev): 4.9493/0.00
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Extra file open flags: (none)
128 files, 16MiB each
2GiB total file size
Block size 16KiB
Number of IO requests: 0
Read/Write ratio for combined random IO test: 1.50
Periodic FSYNC enabled, calling fsync() each 100 requests.
Calling fsync() at the end of test, Enabled.
Using synchronous I/O mode
Doing random r/w test
Initializing worker threads...
Threads started!
File operations:
reads/s: 789.46
writes/s: 526.31
fsyncs/s: 1691.85
Throughput:
read, MiB/s: 12.34
written, MiB/s: 8.22
General statistics:
total time: 10.0292s
total number of events: 30045
Latency (ms):
min: 0.00
avg: 0.33
max: 17.49
95th percentile: 2.48
sum: 9966.50
Threads fairness:
events (avg/stddev): 30045.0000/0.00
execution time (avg/stddev): 9.9665/0.00

View File

@@ -0,0 +1,114 @@
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Prime numbers limit: 10000
Initializing worker threads...
Threads started!
CPU speed:
events per second: 552.76
General statistics:
total time: 10.0015s
total number of events: 5530
Latency (ms):
min: 1.79
avg: 1.81
max: 2.34
95th percentile: 1.82
sum: 9999.03
Threads fairness:
events (avg/stddev): 5530.0000/0.00
execution time (avg/stddev): 9.9990/0.00
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Running memory speed test with the following options:
block size: 1KiB
total size: 102400MiB
operation: write
scope: global
Initializing worker threads...
Threads started!
Total operations: 28620880 (2861268.15 per second)
27950.08 MiB transferred (2794.21 MiB/sec)
General statistics:
total time: 10.0001s
total number of events: 28620880
Latency (ms):
min: 0.00
avg: 0.00
max: 0.10
95th percentile: 0.00
sum: 5501.03
Threads fairness:
events (avg/stddev): 28620880.0000/0.00
execution time (avg/stddev): 5.5010/0.00
sysbench 1.0.20 (using bundled LuaJIT 2.1.0-beta2)
Running the test with following options:
Number of threads: 1
Initializing random number generator from current time
Extra file open flags: (none)
128 files, 16MiB each
2GiB total file size
Block size 16KiB
Number of IO requests: 0
Read/Write ratio for combined random IO test: 1.50
Periodic FSYNC enabled, calling fsync() each 100 requests.
Calling fsync() at the end of test, Enabled.
Using synchronous I/O mode
Doing random r/w test
Initializing worker threads...
Threads started!
File operations:
reads/s: 785.13
writes/s: 523.42
fsyncs/s: 1687.54
Throughput:
read, MiB/s: 12.27
written, MiB/s: 8.18
General statistics:
total time: 10.0083s
total number of events: 29866
Latency (ms):
min: 0.00
avg: 0.33
max: 18.46
95th percentile: 2.48
sum: 9963.06
Threads fairness:
events (avg/stddev): 29866.0000/0.00
execution time (avg/stddev): 9.9631/0.00

View File

@@ -0,0 +1,8 @@
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4ff5ebc88a1f code-backend_reconaissance "python main.py" 3 hours ago Up 2 hours 0.0.0.0:5000->5000/tcp, :::5000->5000/tcp backend_reconaissance
02e8569d1926 code-video_loopback "ffmpeg -video_size …" 3 hours ago Up 2 hours video_loopback
d50295f99ae5 phpmyadmin:latest "/docker-entrypoint.…" 4 hours ago Up 2 hours 0.0.0.0:8000->80/tcp, :::8000->80/tcp phpmyadmin
a5efc4ddae1b code-reviews_api "docker-entrypoint.s…" 4 hours ago Up 2 hours 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp reviews_api
830786df4f5f httpd:latest "httpd-foreground" 4 hours ago Up 2 hours 0.0.0.0:8888->80/tcp, :::8888->80/tcp interface_borne
2fd04a8fe768 mysql:latest "docker-entrypoint.s…" 4 hours ago Up 2 hours (healthy) 3306/tcp, 33060/tcp db
9999c72bb59f httpd:latest "httpd-foreground" 4 hours ago Up 2 hours 0.0.0.0:80->80/tcp, :::80->80/tcp interface_admin

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,111 @@
# Rapport PAN2
## Choix du materiel
* **Ecran** (InnoLux N133HSE): Pour l'écran nous avons choisi un écran 13.3" (non tactile), nous avons estimé la taille suffisante pour un confort de lecture à distance raisonable de la borne. De plus cet écran présente un système de montage à vis qui facilitera son intégration dans la borne.
* **Camera** : Nous avions le choix entre 2 camera
* Une webcam Logitech c525
* Grand choix de résolutions avec un framerate élevé par exemple 640x480@30fps, 960x720@30fps, 1920x1080@5fps
* Mais champs de vision beaucoup trop faible pour notre application
* Une camera de surveillance grand angle de la marque ELP
* Choix de résoltions restreint, mais utilisable dans des bonnes conditions de luminosité
<img src="img/formats_camera.png" height=250>
* Champs de vision adapté a notre utilisation
* Cette camera apporte des distortions importantes sur les bords, cela compléxifie la reconnaissance d'image.
* Nous avons choisi la deuxième camera pour les raisons citées ci-dessus
* **Ordinateur** :
Nous avons à notre disposition un AMD Cubi doté de 4Go de RAM, 128Go de SSD et un Intel Core i3 5005U. L'objectif du reste de ce document est d'évaluer si les performances de cet ordinateur sont suffisantes pour notre application.
## Choix du système d'exploitation
Nous avons choisi d'installer debian 11, une distribution de linux légère avec laquelle nous étions familiers. Pour l'environement de bureau nous avons choisi LXDE là aussi pour le minimalisme qu'il présente au vu des performances de la machine. Nous avons par la suite désinstallé tout les packets installé par défaut par LXDE dont nous n'avions pas besoin. (Par exemple les jeux, la calculatrice, etc.)
## Benchmarking
Pour évaluer la capacité du materiel à supporter la charge de notre application, nous avons executé en parallèle les applications qui seront utilisés pour le produit final ou une application aux besoins équivalents quand cela n'était pas possible. De ce fait ce benchmark n'est pas exact mais nous donnera un ordre d'idée sur les besoins de notre projet.
### Liste des modules à executer
Pour ce faire nous avons mis en place un container Docker par module de notre application :
* **db** : un serveur mysql basique pour la base de donnée
* **phpmyadmin** : l'interface phpmyadmin pour gérer la base de donnée. Ce container n'est pas absolument nécessaire au produit final mais permet d'administrer facilement la base de donnée
* **review_api** : un serveur express pour servir l'api permettant de récupérer et ajouter des avis dans la base de donnée, ainsi que de calculer les statistiques.
* **interface_borne**: un serveur apache2 permettant de servir l'interface graphique de la borne.
* **interface_admin**: idem pour l'interface graphique, ce serveur pourait être fusioné avec
**interface_borne** mais dans le cadre de ce benchmarking on les gardera séparés.
* **backend_reconnaissance**: Ce container s'occupera de la reconnaissance audio et video de la borne. Cependant ces deux processus ne seront jamais actifs en même temps, de plus la reconnaissance d'image sera la plus couteuse en terme de puissance de calcul. C'est pour cela qu'ici nous avons uniquement utilisé mediapipe hands (bibliothèque python de reconaissance de mains) avec une implémentation de la communication avec l'interface de la borne comme programme équivalent.
* **video_loopback** : ce container sert à contourner un problème que nous avons rencontré avec la gestion des camera par linux. En effet seul un programme peut accéder au flux d'une camera à la fois. C'est pour cela que nous avons utilisé `v4l2loopback` avec `ffmpeg` pour dupliquer le flux de notre camera dans 2 cameras virtuelles.
En parallèle firefox est ouvert pour afficher l'interface graphique de la borne.
### Résultats
En réglant la camera a 640x480@30fps, aucune perte d'image n'est observée dans le retour vidéo dans firefox.
Nous avons observé la capacité de Mediapipe l'image de la camera et de la communiquer à l'interface web qui affiche le résultat.
Nous avons obtenu les résultats suivants pour 2 modes de gestion de fréquence du processeur (la gestions automatique de base schedutil et performance qui utillise la fréquence maximale)
| CPU Scheduler | FPS moyens | Ecart type |
|--------------|------------|------------|
| schedutil |10.4 | 1.4 |
| performance | 10.1 | 0.9 |
Ce taux de rafraichissement est suffisant pour que l'application paraisse relativement réactive à l'utilisateur, bien que cela ne soit pas du temps réel.
Pendant ce temps l'api de traitement des avis et la base de donnée fonctionnent correctement en affichant une lattence de 8ms pour une récupération de la liste d'avis.
Pendance ce temps l'utilisation du processeur qui varie de 250% à 280% (sur 400% pour les 4 coeurs) et une utilisation de la RAM de 50% (1.96Go) ce qui nous laisse de la marge en cas d'ajout imprévu.
Les processus utilisant le plus de CPU sont la reconaissance d'image (70%) et firefox pour afficher l'interface de la borne (70-80%). En cas de besoin ces valeurs pourront être diminuées au prix de la fluidité du retour vidéo.
Pour la RAM c'est le serveur mysql (10%) et firefox (10%) qui consomment le plus.
Pour ce qui est de la température, comme la borne sera dans un environement fermé, il était important de tester le bon fonctionnement du materiel dans ces conditions. Nous avons laissé tourner l'application pendant 2h dans une boite en carton fermée. Au début du test la température du processeur était de 50°C, au bout de 2h la température était montée a 70°C, ce qui reste assez faible pour ne pas limiter les performances du CPU.
```
$ sensors
acpitz-acpi-0
Adapter: ACPI interface
temp1: +27.8°C (crit = +110.0°C)
temp2: +29.8°C (crit = +110.0°C)
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +69.0°C (high = +105.0°C, crit = +105.0°C)
Core 0: +69.0°C (high = +105.0°C, crit = +105.0°C)
Core 1: +68.0°C (high = +105.0°C, crit = +105.0°C)
```
<div style="page-break-after: always;"></div>
## Impact de l'utilisation de Docker
Nous nous sommes également posé la question de l'impact de l'utilisation de docker dans les performances de notre projet. Pour mesurer cela, nous avons effectué des benchmark directement sur le système puis dans un container Docker pour mesurer la différence. Nous avons utlisé sysbench pour évaluer les performances du CPU, de la RAM et du disque (écriture/lecture aléatoire).
Le script permettant de faire le benchmark
```sh
sysbench --test=cpu run >>sysbench.log
sysbench --test=memory run >>sysbench.log
sysbench --test=fileio --file-test-mode=rndrw prepare
sysbench --test=fileio --file-test-mode=rndrw run >>sysbench.log
sysbench --test=fileio cleanup
```
Le Dockerfile du container dans lequel nous avons exectué le même script
```Dockerfile
FROM alpine:latest
RUN apk add --no-cache sysbench
WORKDIR /app
COPY benchmark_script.sh /app/benchmark_script.sh
CMD ["sh","benchmark_script.sh"]
```
Les résultats de ce test on permis de conclure que l'impact de docker était négligeable.
<img src="img/benchmark.png" width=400>
| Type | CPU (Evts/s) | RAM (Mbi/s) | Disque lecture(Mbi/s) | Disque écriture (Mbi/s) |
|--------------|------------|------------|---|---|
| Normal | 613.14 | 3259.84 | 12.34 | 8.22 |
| Docker | 552.76 | 2794.21 |12.27 | 8.18 |
## Conclusion
Au vu des tests effectués le materiel dont nous disposons semble adapté à notre projet. Il serait cependant possible de réduire l'utilisation faite de l'ordinateur embarqué dans la borne en déplaçant la partie stockage et traitement des avis sur un autre serveur.

Binary file not shown.

View File

@@ -0,0 +1,11 @@
acpitz-acpi-0
Adapter: ACPI interface
temp1: +27.8°C (crit = +110.0°C)
temp2: +29.8°C (crit = +110.0°C)
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +69.0°C (high = +105.0°C, crit = +105.0°C)
Core 0: +69.0°C (high = +105.0°C, crit = +105.0°C)
Core 1: +68.0°C (high = +105.0°C, crit = +105.0°C)