Ajout des nouvelles modifications

This commit is contained in:
2026-03-29 14:03:19 +02:00
parent 90f10674a3
commit 98825db072
7 changed files with 514 additions and 284 deletions

View File

@@ -1,15 +1,22 @@
version: '3.8' version: '3.8'
# ============================================================
# Smart Parking v2.0 — Docker Compose
# Services : MariaDB + App Node.js + Mosquitto MQTT
# ============================================================
services: services:
# ── Base de données MariaDB ────────────────────────────────
db: db:
image: mariadb:10.11 image: mariadb:10.11
container_name: smartparking-db container_name: smartparking-db
restart: always restart: always
environment: environment:
MARIADB_ROOT_PASSWORD: rootpassword # À changer MARIADB_ROOT_PASSWORD: rootpassword # ⚠️ À changer en production
MARIADB_DATABASE: smartparking MARIADB_DATABASE: smartparking
MARIADB_USER: smartparking_user MARIADB_USER: smartparking_user
MARIADB_PASSWORD: smartparking_pass # À changer MARIADB_PASSWORD: smartparking_pass # ⚠️ À changer en production
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql - ./init.sql:/docker-entrypoint-initdb.d/init.sql
@@ -20,6 +27,25 @@ services:
timeout: 10s timeout: 10s
retries: 5 retries: 5
# ── Broker MQTT Mosquitto ──────────────────────────────────
# Si vous avez déjà Mosquitto installé directement sur le Pi
# (pas dans Docker), commentez ce bloc et mettez
# MQTT_HOST=localhost dans la section "app" ci-dessous.
mqtt:
image: eclipse-mosquitto:2
container_name: smartparking-mqtt
restart: always
ports:
- "1883:1883" # Port MQTT (Arduino se connecte ici)
- "9001:9001" # Port WebSocket (optionnel)
volumes:
- ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf
- mosquitto_data:/mosquitto/data
- mosquitto_log:/mosquitto/log
networks:
- smartparking-network
# ── Application Node.js ────────────────────────────────────
app: app:
build: . build: .
container_name: smartparking-app container_name: smartparking-app
@@ -29,19 +55,31 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
mqtt:
condition: service_started
environment: environment:
DB_HOST: db # Base de données
DB_PORT: 3306 DB_HOST: db
DB_USER: smartparking_user DB_PORT: 3306
DB_USER: smartparking_user
DB_PASSWORD: smartparking_pass DB_PASSWORD: smartparking_pass
DB_NAME: smartparking DB_NAME: smartparking
JWT_SECRET: ${JWT_SECRET:-une_chaine_tres_longue_et_secrete} # JWT
JWT_SECRET: ${JWT_SECRET:-une_chaine_tres_longue_et_secrete_changez_moi}
# MQTT — utiliser "mqtt" si Mosquitto est dans Docker
# utiliser "localhost" si Mosquitto est installé directement sur le Pi
MQTT_HOST: mqtt
MQTT_PORT: 1883
# Environnement
NODE_ENV: production NODE_ENV: production
PORT: 3000
networks: networks:
- smartparking-network - smartparking-network
volumes: volumes:
db_data: db_data:
mosquitto_data:
mosquitto_log:
networks: networks:
smartparking-network: smartparking-network:

356
js/map.js
View File

@@ -1,110 +1,153 @@
/** /**
* ============================================ * ============================================
* MAP.JS - Carte des places de parking * MAP.JS - Carte des places de parking
* Smart Parking - BTS CIEL IR * Smart Parking v2.0
* MODIFIÉ : polling API toutes les 3s pour recevoir
* les mises à jour en temps réel depuis Arduino
* ============================================ * ============================================
*/ */
// Configuration
const MAP_CONFIG = { const MAP_CONFIG = {
totalSpots: 10, // Nombre total de places totalSpots: 10,
updateInterval: 5000 // Intervalle de mise à jour updateInterval: 3000 // Refresh depuis l'API toutes les 3 secondes
}; };
// État des places
let spotsState = { let spotsState = {
spots: [], spots: [],
selectedSpot: null selectedSpot: null
}; };
// Types de places const SPOT_STATUS = { FREE: 'free', OCCUPIED: 'occupied', RESERVED: 'reserved' };
const SPOT_STATUS = {
FREE: 'free', // ============================================
OCCUPIED: 'occupied', // INITIALISATION
RESERVED: 'reserved' // ============================================
};
// Initialisation
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('🗺️ Initialisation de la carte...'); console.log('🗺️ Initialisation de la carte...');
initParkingMap(); initParkingMap();
}); });
/** async function initParkingMap() {
* Initialise la carte du parking // Essayer d'abord de charger depuis l'API (données MariaDB + Arduino)
*/ const loaded = await loadSpotsFromAPI();
function initParkingMap() {
// Charger les places depuis le stockage local ou créer les places par défaut // Si pas d'API disponible, utiliser le localStorage (mode offline)
loadSpots(); if (!loaded) {
loadSpotsFromStorage();
}
// Rendre la carte
renderMap(); renderMap();
// Mettre à jour les statistiques
updateStats(); updateStats();
// Mettre à jour le formulaire de réservation
updateReservationForm(); updateReservationForm();
// Démarrer la simulation (si pas admin) // ⭐ Polling toutes les 3 secondes pour recevoir les updates Arduino
if (!isAdmin()) { startAPIPolling();
startSimulation(); }
// ============================================
// CHARGEMENT DES PLACES
// ============================================
/**
* Charge les places depuis l'API (MariaDB)
* Retourne true si succès, false si hors-ligne
*/
async function loadSpotsFromAPI() {
try {
const token = localStorage.getItem('smart_parking_token');
if (!token) return false;
const response = await fetch('/api/spots', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (!response.ok) return false;
const data = await response.json();
if (!data.success || !data.data.length) return false;
// Convertir le format API → format interne
spotsState.spots = data.data.map(s => ({
id: s.id,
number: s.number,
status: s.status,
lastUpdate: s.last_update,
sensorId: s.sensor_id
}));
// Synchroniser avec localStorage pour le mode offline
saveSpots();
return true;
} catch (_err) {
// Serveur non joignable → mode offline
return false;
} }
} }
/** /**
* Charge les places * Charge les places depuis le localStorage (mode offline)
*/ */
function loadSpots() { function loadSpotsFromStorage() {
const stored = localStorage.getItem('smart_parking_spots'); const stored = localStorage.getItem('smart_parking_spots');
if (stored) { if (stored) {
spotsState.spots = JSON.parse(stored); spotsState.spots = JSON.parse(stored);
} else { } else {
// Créer les places par défaut
createDefaultSpots(); createDefaultSpots();
} }
} }
/**
* Crée les places par défaut
*/
function createDefaultSpots() { function createDefaultSpots() {
spotsState.spots = []; spotsState.spots = [];
for (let i = 1; i <= MAP_CONFIG.totalSpots; i++) { for (let i = 1; i <= MAP_CONFIG.totalSpots; i++) {
// Distribution: 60% libre, 25% occupé, 15% réservé
const rand = Math.random();
let status = SPOT_STATUS.FREE;
if (rand > 0.85) {
status = SPOT_STATUS.RESERVED;
} else if (rand > 0.60) {
status = SPOT_STATUS.OCCUPIED;
}
spotsState.spots.push({ spotsState.spots.push({
id: i, id: i,
number: i, number: i,
status: status, status: SPOT_STATUS.FREE,
lastUpdate: new Date().toISOString(), lastUpdate: new Date().toISOString(),
sensorId: `SENSOR_${String(i).padStart(3, '0')}` sensorId: `SENSOR_${String(i).padStart(3, '0')}`
}); });
} }
saveSpots(); saveSpots();
} }
/**
* Sauvegarde les places
*/
function saveSpots() { function saveSpots() {
localStorage.setItem('smart_parking_spots', JSON.stringify(spotsState.spots)); localStorage.setItem('smart_parking_spots', JSON.stringify(spotsState.spots));
} }
// ============================================
// ⭐ POLLING TEMPS RÉEL (mises à jour Arduino)
// ============================================
/** /**
* Rend la carte * Interroge l'API toutes les 3 secondes.
* Si l'Arduino a changé l'état d'une place via MQTT,
* la carte se met à jour automatiquement.
*/ */
function startAPIPolling() {
setInterval(async () => {
const loaded = await loadSpotsFromAPI();
if (loaded) {
renderMap();
updateStats();
updateReservationForm();
// Rafraîchir les détails si une place est sélectionnée
if (spotsState.selectedSpot) {
const updated = spotsState.spots.find(s => s.id === spotsState.selectedSpot.id);
if (updated) {
spotsState.selectedSpot = updated;
showSpotDetails(updated);
}
}
}
}, MAP_CONFIG.updateInterval);
}
// ============================================
// RENDU DE LA CARTE
// ============================================
function renderMap() { function renderMap() {
const mapContainer = document.getElementById('parkingMap'); const mapContainer = document.getElementById('parkingMap');
if (!mapContainer) return; if (!mapContainer) return;
@@ -114,7 +157,7 @@ function renderMap() {
class="parking-spot ${spot.status}" class="parking-spot ${spot.status}"
data-id="${spot.id}" data-id="${spot.id}"
onclick="handleSpotClick(${spot.id})" onclick="handleSpotClick(${spot.id})"
title="Place ${spot.number} - ${getStatusLabel(spot.status)}" title="Place ${spot.number} ${getStatusLabel(spot.status)}"
> >
<span class="spot-number">${spot.number}</span> <span class="spot-number">${spot.number}</span>
<span class="spot-icon">${getStatusIcon(spot.status)}</span> <span class="spot-icon">${getStatusIcon(spot.status)}</span>
@@ -122,26 +165,20 @@ function renderMap() {
`).join(''); `).join('');
} }
/**
* Gère le clic sur une place
*/
function handleSpotClick(spotId) { function handleSpotClick(spotId) {
const spot = spotsState.spots.find(s => s.id === spotId); const spot = spotsState.spots.find(s => s.id === spotId);
if (!spot) return; if (!spot) return;
spotsState.selectedSpot = spot; spotsState.selectedSpot = spot;
showSpotDetails(spot); showSpotDetails(spot);
} }
/**
* Affiche les détails d'une place
*/
function showSpotDetails(spot) { function showSpotDetails(spot) {
const container = document.getElementById('spotDetails'); const container = document.getElementById('spotDetails');
if (!container) return; if (!container) return;
const isReserved = spot.status === SPOT_STATUS.RESERVED; const reservation = spot.status === SPOT_STATUS.RESERVED
const reservation = isReserved ? findReservationForSpot(spot.id) : null; ? findReservationForSpot(spot.id)
: null;
container.innerHTML = ` container.innerHTML = `
<div class="spot-info-detail"> <div class="spot-info-detail">
@@ -157,7 +194,7 @@ function showSpotDetails(spot) {
</div> </div>
<div class="spot-info-row"> <div class="spot-info-row">
<span class="spot-info-label">Capteur</span> <span class="spot-info-label">Capteur</span>
<span class="spot-info-value">${spot.sensorId}</span> <span class="spot-info-value">${spot.sensorId || 'N/A'}</span>
</div> </div>
<div class="spot-info-row"> <div class="spot-info-row">
<span class="spot-info-label">Dernière mise à jour</span> <span class="spot-info-label">Dernière mise à jour</span>
@@ -174,7 +211,8 @@ function showSpotDetails(spot) {
</div> </div>
` : ''} ` : ''}
${spot.status === SPOT_STATUS.FREE ? ` ${spot.status === SPOT_STATUS.FREE ? `
<button class="btn btn-primary btn-block" onclick="Dashboard.navigateToPage('reservation'); selectSpotForReservation(${spot.id});"> <button class="btn btn-primary btn-block"
onclick="Dashboard.navigateToPage('reservation'); selectSpotForReservation(${spot.id});">
<span class="btn-icon">📅</span> <span class="btn-icon">📅</span>
Réserver cette place Réserver cette place
</button> </button>
@@ -183,184 +221,144 @@ function showSpotDetails(spot) {
`; `;
} }
/**
* Trouve la réservation pour une place
*/
function findReservationForSpot(spotId) { function findReservationForSpot(spotId) {
const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]'); const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
return reservations.find(r => r.spotId === spotId && r.status === 'active'); return reservations.find(r => r.spotId === spotId && r.status === 'active');
} }
/** // ============================================
* Met à jour les statistiques // STATISTIQUES & FORMULAIRE
*/ // ============================================
function updateStats() { function updateStats() {
const free = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length; const free = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length;
const occupied = spotsState.spots.filter(s => s.status === SPOT_STATUS.OCCUPIED).length; const occupied = spotsState.spots.filter(s => s.status === SPOT_STATUS.OCCUPIED).length;
const reserved = spotsState.spots.filter(s => s.status === SPOT_STATUS.RESERVED).length; const reserved = spotsState.spots.filter(s => s.status === SPOT_STATUS.RESERVED).length;
document.getElementById('freeCount').textContent = free; document.getElementById('freeCount').textContent = free;
document.getElementById('occupiedCount').textContent = occupied; document.getElementById('occupiedCount').textContent = occupied;
document.getElementById('reservedCount').textContent = reserved; document.getElementById('reservedCount').textContent = reserved;
document.getElementById('totalCount').textContent = spotsState.spots.length; document.getElementById('totalCount').textContent = spotsState.spots.length;
} }
/**
* Met à jour le formulaire de réservation
*/
function updateReservationForm() { function updateReservationForm() {
const select = document.getElementById('resSpot'); const select = document.getElementById('resSpot');
if (!select) return; if (!select) return;
// Garder la première option
const firstOption = select.options[0]; const firstOption = select.options[0];
select.innerHTML = ''; select.innerHTML = '';
select.appendChild(firstOption); select.appendChild(firstOption);
// Ajouter uniquement les places libres spotsState.spots
const freeSpots = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE); .filter(s => s.status === SPOT_STATUS.FREE)
.forEach(spot => {
freeSpots.forEach(spot => { const option = document.createElement('option');
const option = document.createElement('option'); option.value = spot.id;
option.value = spot.id; option.textContent = `Place ${spot.number}`;
option.textContent = `Place ${spot.number}`; select.appendChild(option);
select.appendChild(option); });
});
} }
/**
* Sélectionne une place pour la réservation
*/
function selectSpotForReservation(spotId) { function selectSpotForReservation(spotId) {
const select = document.getElementById('resSpot'); const select = document.getElementById('resSpot');
if (select) { if (select) select.value = spotId;
select.value = spotId;
}
} }
/** // ============================================
* Définit le statut d'une place // ACTIONS ADMIN & SIMULATION
*/ // ============================================
function setSpotStatus(spotId, status) {
async function setSpotStatus(spotId, status) {
const spot = spotsState.spots.find(s => s.id === spotId || s.number === spotId); const spot = spotsState.spots.find(s => s.id === spotId || s.number === spotId);
if (spot) { if (!spot) return;
spot.status = status;
spot.lastUpdate = new Date().toISOString();
saveSpots();
renderMap();
updateStats();
updateReservationForm();
}
}
/** spot.status = status;
* Change le nombre total de places spot.lastUpdate = new Date().toISOString();
*/ saveSpots();
function setTotalSpots(count) { renderMap();
MAP_CONFIG.totalSpots = count; updateStats();
updateReservationForm();
// Ajuster le tableau des places // Synchroniser avec l'API si connecté
if (count > spotsState.spots.length) { try {
// Ajouter des places const token = localStorage.getItem('smart_parking_token');
for (let i = spotsState.spots.length + 1; i <= count; i++) { if (token) {
spotsState.spots.push({ await fetch(`/api/spots/${spot.id}/status`, {
id: i, method: 'PUT',
number: i, headers: {
status: SPOT_STATUS.FREE, 'Content-Type': 'application/json',
lastUpdate: new Date().toISOString(), 'Authorization': 'Bearer ' + token
sensorId: `SENSOR_${String(i).padStart(3, '0')}` },
body: JSON.stringify({ status })
}); });
} }
} else if (count < spotsState.spots.length) { } catch (_err) { /* mode offline */ }
// Supprimer des places }
function setTotalSpots(count) {
MAP_CONFIG.totalSpots = count;
if (count > spotsState.spots.length) {
for (let i = spotsState.spots.length + 1; i <= count; i++) {
spotsState.spots.push({
id: i, number: i,
status: SPOT_STATUS.FREE,
lastUpdate: new Date().toISOString(),
sensorId: `SENSOR_${String(i).padStart(3, '0')}`
});
}
} else {
spotsState.spots = spotsState.spots.slice(0, count); spotsState.spots = spotsState.spots.slice(0, count);
} }
saveSpots(); saveSpots();
renderMap(); renderMap();
updateStats(); updateStats();
updateReservationForm(); updateReservationForm();
} }
/** // ============================================
* Simulation automatique // UTILITAIRES
*/ // ============================================
function startSimulation() {
setInterval(() => {
// 20% de chance de changer une place
if (Math.random() > 0.8) {
const randomSpot = spotsState.spots[Math.floor(Math.random() * spotsState.spots.length)];
if (randomSpot.status === SPOT_STATUS.FREE && Math.random() > 0.5) {
setSpotStatus(randomSpot.id, SPOT_STATUS.OCCUPIED);
} else if (randomSpot.status === SPOT_STATUS.OCCUPIED && Math.random() > 0.3) {
setSpotStatus(randomSpot.id, SPOT_STATUS.FREE);
}
}
}, MAP_CONFIG.updateInterval);
}
/**
* Vérifie si l'utilisateur est admin
*/
function isAdmin() { function isAdmin() {
const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null'); const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null');
return user && user.role === 'admin'; return user && user.role === 'admin';
} }
/**
* Retourne le label du statut
*/
function getStatusLabel(status) { function getStatusLabel(status) {
const labels = { return { free: 'Libre', occupied: 'Occupée', reserved: 'Réservée' }[status] || 'Inconnu';
[SPOT_STATUS.FREE]: 'Libre',
[SPOT_STATUS.OCCUPIED]: 'Occupée',
[SPOT_STATUS.RESERVED]: 'Réservée'
};
return labels[status] || 'Inconnu';
} }
/**
* Retourne l'icône du statut
*/
function getStatusIcon(status) { function getStatusIcon(status) {
const icons = { return { free: '✓', occupied: '🚗', reserved: '📅' }[status] || '?';
[SPOT_STATUS.FREE]: '✓',
[SPOT_STATUS.OCCUPIED]: '🚗',
[SPOT_STATUS.RESERVED]: '📅'
};
return icons[status] || '?';
} }
/**
* Formate une date
*/
function formatDate(dateString) { function formatDate(dateString) {
const date = new Date(dateString); if (!dateString) return 'N/A';
return date.toLocaleString('fr-FR', { return new Date(dateString).toLocaleString('fr-FR', {
day: '2-digit', day: '2-digit', month: '2-digit', year: 'numeric',
month: '2-digit', hour: '2-digit', minute: '2-digit'
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}); });
} }
// Exporter les fonctions // ============================================
// EXPORT
// ============================================
window.ParkingMap = { window.ParkingMap = {
refresh: () => { refresh: async () => {
loadSpots(); const loaded = await loadSpotsFromAPI();
if (!loaded) loadSpotsFromStorage();
renderMap(); renderMap();
updateStats(); updateStats();
updateReservationForm(); updateReservationForm();
}, },
setSpotStatus, setSpotStatus,
setTotalSpots, setTotalSpots,
getSpots: () => spotsState.spots, getSpots: () => spotsState.spots,
getFreeSpots: () => spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE), getFreeSpots: () => spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE),
getStats: () => ({ getStats: () => ({
total: spotsState.spots.length, total: spotsState.spots.length,
free: spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length, free: spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length,
occupied: spotsState.spots.filter(s => s.status === SPOT_STATUS.OCCUPIED).length, occupied: spotsState.spots.filter(s => s.status === SPOT_STATUS.OCCUPIED).length,
reserved: spotsState.spots.filter(s => s.status === SPOT_STATUS.RESERVED).length reserved: spotsState.spots.filter(s => s.status === SPOT_STATUS.RESERVED).length
}) })

View File

@@ -0,0 +1,24 @@
# ============================================================
# Configuration Mosquitto - Smart Parking
# Fichier : mosquitto/config/mosquitto.conf
# ============================================================
# Port d'écoute MQTT standard
listener 1883
# Port WebSocket (optionnel, pour tests depuis navigateur)
listener 9001
protocol websockets
# Autoriser les connexions sans authentification
# (à sécuriser en production avec un fichier password)
allow_anonymous true
# Persistance des messages
persistence true
persistence_location /mosquitto/data/
# Logs
log_dest file /mosquitto/log/mosquitto.log
log_dest stdout
log_type all

6
package-lock.json generated
View File

@@ -1,6 +0,0 @@
{
"name": "Parking",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -1,8 +1,8 @@
/** /**
* ============================================ * ============================================
* DATABASE.JS - Gestion MariaDB * DATABASE.JS - Gestion MariaDB
* Smart Parking - BTS CIEL IR * Smart Parking v2.0
* MODIFIÉ : ajout de getReservationById * AJOUTÉ : expireReservations() — libère auto les places
* ============================================ * ============================================
*/ */
@@ -11,16 +11,20 @@ const bcrypt = require('bcryptjs');
require('dotenv').config(); require('dotenv').config();
const pool = mysql.createPool({ const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost', host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 3306, port: process.env.DB_PORT || 3306,
user: process.env.DB_USER || 'smartparking_user', user: process.env.DB_USER || 'smartparking_user',
password: process.env.DB_PASSWORD || 'smartparking_pass', password: process.env.DB_PASSWORD || 'smartparking_pass',
database: process.env.DB_NAME || 'smartparking', database: process.env.DB_NAME || 'smartparking',
waitForConnections: true, waitForConnections: true,
connectionLimit: 10, connectionLimit: 10,
queueLimit: 0 queueLimit: 0
}); });
// ============================================
// INITIALISATION
// ============================================
async function initDatabase() { async function initDatabase() {
try { try {
await pool.query(` await pool.query(`
@@ -97,9 +101,9 @@ async function initDatabase() {
) )
`); `);
console.log('✅ Tables vérifiées/créées avec succès'); console.log('✅ Tables vérifiées/créées');
// Compte admin par défaut // Admin par défaut
const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', ['admin@smartparking.fr']); const [rows] = await pool.query('SELECT id FROM users WHERE email = ?', ['admin@smartparking.fr']);
if (rows.length === 0) { if (rows.length === 0) {
const hashed = await bcrypt.hash('admin123', 10); const hashed = await bcrypt.hash('admin123', 10);
@@ -107,27 +111,23 @@ async function initDatabase() {
'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)',
['Administrateur', 'admin@smartparking.fr', '01 23 45 67 89', hashed, 'admin'] ['Administrateur', 'admin@smartparking.fr', '01 23 45 67 89', hashed, 'admin']
); );
console.log('✅ Administrateur par défaut créé'); console.log('✅ Admin par défaut créé');
} }
// Places par défaut // 10 places par défaut
const [spots] = await pool.query('SELECT COUNT(*) AS count FROM spots'); const [spots] = await pool.query('SELECT COUNT(*) AS count FROM spots');
if (spots[0].count === 0) { if (spots[0].count === 0) {
for (let i = 1; i <= 10; i++) { for (let i = 1; i <= 10; i++) {
let status = 'free';
const rand = Math.random();
if (rand > 0.85) status = 'reserved';
else if (rand > 0.60) status = 'occupied';
await pool.query( await pool.query(
'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)', 'INSERT INTO spots (number, sensor_id, status) VALUES (?, ?, ?)',
[i, `SENSOR_${String(i).padStart(3, '0')}`, status] [i, `SENSOR_${String(i).padStart(3, '0')}`, 'free']
); );
} }
console.log('✅ 10 places par défaut créées'); console.log('✅ 10 places créées (toutes libres)');
} }
} catch (err) { } catch (err) {
console.error("❌ Erreur lors de l'initialisation de la base :", err.message); console.error("❌ Erreur init base :", err.message);
throw err; throw err;
} }
} }
@@ -166,8 +166,10 @@ async function getAllUsers() {
async function updateUser(id, updates) { async function updateUser(id, updates) {
const fields = Object.keys(updates).map(k => `${k} = ?`).join(', '); const fields = Object.keys(updates).map(k => `${k} = ?`).join(', ');
const values = Object.values(updates); const [result] = await pool.query(
const [result] = await pool.query(`UPDATE users SET ${fields} WHERE id = ?`, [...values, id]); `UPDATE users SET ${fields} WHERE id = ?`,
[...Object.values(updates), id]
);
return { changed: result.affectedRows }; return { changed: result.affectedRows };
} }
@@ -225,10 +227,6 @@ async function createReservation(userId, spotId, date, startTime, endTime, durat
return { id: result.insertId }; return { id: result.insertId };
} }
/**
* Récupère une réservation par son ID
* AJOUTÉ : nécessaire pour l'annulation/complétion (libération de la place)
*/
async function getReservationById(id) { async function getReservationById(id) {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT r.*, s.number AS spot_number `SELECT r.*, s.number AS spot_number
@@ -271,6 +269,53 @@ async function updateReservationStatus(id, status) {
return { changed: result.affectedRows }; return { changed: result.affectedRows };
} }
/**
* ⭐ NOUVELLE FONCTION — Expiration automatique des réservations
*
* Cherche toutes les réservations actives dont la date+heure de fin
* est déjà dépassée, les passe en "completed" et libère les places.
*
* Appelée toutes les 60 secondes par server.js.
* Cela résout le problème des places qui restent "réservées" indéfiniment.
*/
async function expireReservations() {
// Trouver les réservations actives dont l'heure de fin est passée
const [expiredRows] = await pool.query(`
SELECT r.id, r.spot_id, r.user_id, s.number AS spot_number
FROM reservations r
JOIN spots s ON r.spot_id = s.id
WHERE r.status = 'active'
AND TIMESTAMP(r.date, r.end_time) < NOW()
`);
for (const res of expiredRows) {
// Passer la réservation en "completed"
await pool.query(
"UPDATE reservations SET status = 'completed' WHERE id = ?",
[res.id]
);
// Libérer la place (la passer en "free")
// (le capteur Arduino prendra le relais ensuite si une voiture est encore là)
await pool.query(
"UPDATE spots SET status = 'free', last_update = NOW() WHERE id = ?",
[res.spot_id]
);
// Ajouter à l'historique
await pool.query(
'INSERT INTO history (action, details, user_id) VALUES (?, ?, ?)',
[
'Expiration réservation',
`Réservation #${res.id} expirée — place ${res.spot_number} libérée automatiquement`,
res.user_id
]
);
}
return expiredRows.length;
}
// ============================================ // ============================================
// HISTORIQUE // HISTORIQUE
// ============================================ // ============================================
@@ -319,7 +364,7 @@ async function getStats(days = 7) {
} }
// ============================================ // ============================================
// MQTT // MQTT EVENTS
// ============================================ // ============================================
async function recordMqttEvent(topic, message) { async function recordMqttEvent(topic, message) {
@@ -336,11 +381,11 @@ async function recordMqttEvent(topic, message) {
async function closeDatabase() { async function closeDatabase() {
await pool.end(); await pool.end();
console.log('🔌 Connexions à la base fermées'); console.log('🔌 Connexions base fermées');
} }
module.exports = { module.exports = {
pool, // exposé pour les requêtes directes si besoin pool,
initDatabase, initDatabase,
closeDatabase, closeDatabase,
// Utilisateurs // Utilisateurs
@@ -350,6 +395,7 @@ module.exports = {
// Réservations // Réservations
createReservation, getReservationById, getReservationsByUser, createReservation, getReservationById, getReservationsByUser,
getAllReservations, updateReservationStatus, getAllReservations, updateReservationStatus,
expireReservations, // ← NOUVEAU
// Historique // Historique
addHistory, getHistory, addHistory, getHistory,
// Stats // Stats

View File

@@ -1,12 +1,11 @@
{ {
"name": "smart-parking-server", "name": "smart-parking-server",
"version": "1.0.0", "version": "2.0.0",
"description": "Backend Smart Parking avec MariaDB et Docker", "description": "Backend Smart Parking avec MariaDB, Docker et MQTT Arduino",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"dev": "nodemon server.js", "dev": "nodemon server.js"
"init-db": "node db/init.js"
}, },
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
@@ -16,7 +15,8 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"uuid": "^9.0.1" "uuid": "^9.0.1",
"mqtt": "^5.3.4"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.0.1" "nodemon": "^3.0.1"

View File

@@ -1,70 +1,200 @@
const express = require('express'); /**
const cors = require('cors'); * ============================================
* SERVER.JS - Serveur principal Smart Parking
* VERSION 2.0 - MQTT Arduino + Expiration réservations
* ============================================
*/
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const path = require('path'); const path = require('path');
const mqtt = require('mqtt');
require('dotenv').config(); require('dotenv').config();
const db = require('./db/database'); const db = require('./db/database');
const apiRoutes = require('./routes/api'); const apiRoutes = require('./routes/api');
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
const app = express();
const app = express();
app.use(cors()); app.use(cors());
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, '..'))); app.use(express.static(path.join(__dirname, '..')));
app.use('/api', apiRoutes); app.use('/api', apiRoutes);
app.get('/', (req, res) => { app.get('/', (_req, res) => res.sendFile(path.join(__dirname, '..', 'index.html')));
res.sendFile(path.join(__dirname, '..', 'index.html')); app.get('/dashboard', (_req, res) => res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html')));
});
app.get('/dashboard', (req, res) => { // ============================================
res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html')); // CONNEXION MQTT (Mosquitto sur le Raspberry Pi)
}); // ============================================
async function startServer() { /**
try { * Topics attendus depuis l'Arduino :
await db.initDatabase(); *
app.listen(PORT, () => { * smartparking/sensor/1 → "1" = voiture détectée (occupée)
console.log(` * smartparking/sensor/1 → "0" = place libre
╔══════════════════════════════════════════════════╗ *
║ 🅿️ SMART PARKING SERVER - PRÊT POUR DOCKER ║ * L'Arduino publie sur ce topic via le shield Ethernet/WiFi.
╠══════════════════════════════════════════════════╣ * Le numéro à la fin correspond au numéro de place (1 à N).
║ 🌐 Port : ${PORT} *
║ 🗄️ Base : MariaDB (${process.env.DB_HOST}) * Pour tester sans Arduino (ligne de commande sur le Pi) :
║ 🔐 JWT sécurisé * mosquitto_pub -h localhost -t "smartparking/sensor/1" -m "1"
╚══════════════════════════════════════════════════╝ * mosquitto_pub -h localhost -t "smartparking/sensor/1" -m "0"
`); */
const MQTT_HOST = process.env.MQTT_HOST || 'localhost';
const MQTT_PORT = process.env.MQTT_PORT || 1883;
const MQTT_TOPIC = 'smartparking/sensor/#'; // # = wildcard, reçoit tous les capteurs
let mqttClient = null;
function connectMQTT() {
const brokerUrl = `mqtt://${MQTT_HOST}:${MQTT_PORT}`;
console.log(`🔌 Connexion au broker MQTT : ${brokerUrl}`);
mqttClient = mqtt.connect(brokerUrl, {
clientId: 'smartparking-server-' + Math.random().toString(16).slice(3),
keepalive: 60,
reconnectPeriod: 5000, // Reconnexion automatique toutes les 5s si coupure
connectTimeout: 10000
}); });
setInterval(async () => { mqttClient.on('connect', () => {
try { console.log('✅ MQTT connecté au broker Mosquitto');
const spots = await db.getAllSpots(); mqttClient.subscribe(MQTT_TOPIC, (err) => {
const total = spots.length; if (err) {
const free = spots.filter(s => s.status === 'free').length; console.error('❌ Erreur abonnement MQTT :', err.message);
const occupied = spots.filter(s => s.status === 'occupied').length; } else {
const reserved = spots.filter(s => s.status === 'reserved').length; console.log(`📡 Abonné au topic : ${MQTT_TOPIC}`);
await db.recordStats(total, free, occupied, reserved); }
} catch (err) { });
console.error('❌ Erreur stats:', err.message); });
}
}, 5 * 60 * 1000);
} catch (err) { // Message reçu depuis un capteur Arduino
console.error('❌ Erreur au démarrage :', err); mqttClient.on('message', async (topic, messageBuffer) => {
process.exit(1); const message = messageBuffer.toString().trim();
} const topicParts = topic.split('/'); // ['smartparking', 'sensor', '1']
const spotNumber = parseInt(topicParts[2]); // numéro de place
if (isNaN(spotNumber)) {
console.warn(`⚠️ Topic MQTT invalide : ${topic}`);
return;
}
console.log(`📩 MQTT reçu → topic: ${topic} | valeur: ${message}`);
// "1" = voiture présente → occupée | "0" = libre
const newStatus = message === '1' ? 'occupied' : 'free';
try {
// Récupérer la place par son numéro
const spots = await db.getAllSpots();
const spot = spots.find(s => s.number === spotNumber);
if (!spot) {
console.warn(`⚠️ Place numéro ${spotNumber} introuvable en base`);
return;
}
// Ne pas écraser une place RÉSERVÉE avec un simple signal capteur
// (la réservation prime sur le capteur physique)
if (spot.status === 'reserved' && newStatus === 'occupied') {
console.log(` Place ${spotNumber} déjà réservée — capteur ignoré`);
await db.recordMqttEvent(topic, message);
return;
}
// Mettre à jour en base
await db.updateSpotStatus(spot.id, newStatus);
await db.recordMqttEvent(topic, message);
console.log(`✅ Place ${spotNumber} mise à jour → ${newStatus}`);
} catch (err) {
console.error('❌ Erreur traitement message MQTT :', err.message);
}
});
mqttClient.on('error', (err) => {
console.error('❌ Erreur MQTT :', err.message);
});
mqttClient.on('reconnect', () => {
console.log('🔄 Reconnexion MQTT en cours...');
});
mqttClient.on('offline', () => {
console.warn('⚠️ Client MQTT hors-ligne');
});
} }
// ============================================
// DÉMARRAGE DU SERVEUR
// ============================================
async function startServer() {
try {
// 1. Initialiser la base de données
await db.initDatabase();
// 2. Démarrer le serveur HTTP
app.listen(PORT, () => {
console.log(`
╔══════════════════════════════════════════════════════╗
║ 🅿️ SMART PARKING SERVER v2.0 - PRÊT ║
╠══════════════════════════════════════════════════════╣
║ 🌐 Port HTTP : ${PORT}
║ 🗄️ Base : MariaDB (${process.env.DB_HOST || 'localhost'})
║ 📡 MQTT : ${MQTT_HOST}:${MQTT_PORT}
║ 🔐 JWT sécurisé
╚══════════════════════════════════════════════════════╝
`);
});
// 3. Connecter le client MQTT (broker Mosquitto)
connectMQTT();
// 4. Enregistrer les statistiques toutes les 5 minutes
setInterval(async () => {
try {
const spots = await db.getAllSpots();
const total = spots.length;
const free = spots.filter(s => s.status === 'free').length;
const occupied = spots.filter(s => s.status === 'occupied').length;
const reserved = spots.filter(s => s.status === 'reserved').length;
await db.recordStats(total, free, occupied, reserved);
} catch (err) {
console.error('❌ Erreur stats :', err.message);
}
}, 5 * 60 * 1000);
// 5. ⭐ EXPIRATION AUTOMATIQUE DES RÉSERVATIONS toutes les minutes
// Libère les places dont l'heure de fin est dépassée
setInterval(async () => {
try {
const count = await db.expireReservations();
if (count > 0) {
console.log(`${count} réservation(s) expirée(s) — places libérées`);
}
} catch (err) {
console.error('❌ Erreur expiration réservations :', err.message);
}
}, 60 * 1000); // toutes les 60 secondes
} catch (err) {
console.error('❌ Erreur au démarrage :', err);
process.exit(1);
}
}
// Arrêt propre
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
console.log('\n🛑 Arrêt du serveur...'); console.log('\n🛑 Arrêt du serveur...');
await db.closeDatabase(); if (mqttClient) mqttClient.end();
process.exit(0); await db.closeDatabase();
process.exit(0);
}); });
startServer(); startServer();