Ajout des nouvelles modifications
This commit is contained in:
@@ -1,15 +1,22 @@
|
||||
version: '3.8'
|
||||
|
||||
# ============================================================
|
||||
# Smart Parking v2.0 — Docker Compose
|
||||
# Services : MariaDB + App Node.js + Mosquitto MQTT
|
||||
# ============================================================
|
||||
|
||||
services:
|
||||
|
||||
# ── Base de données MariaDB ────────────────────────────────
|
||||
db:
|
||||
image: mariadb:10.11
|
||||
container_name: smartparking-db
|
||||
restart: always
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: rootpassword # À changer
|
||||
MARIADB_ROOT_PASSWORD: rootpassword # ⚠️ À changer en production
|
||||
MARIADB_DATABASE: smartparking
|
||||
MARIADB_USER: smartparking_user
|
||||
MARIADB_PASSWORD: smartparking_pass # À changer
|
||||
MARIADB_PASSWORD: smartparking_pass # ⚠️ À changer en production
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
@@ -20,6 +27,25 @@ services:
|
||||
timeout: 10s
|
||||
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:
|
||||
build: .
|
||||
container_name: smartparking-app
|
||||
@@ -29,19 +55,31 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
mqtt:
|
||||
condition: service_started
|
||||
environment:
|
||||
# Base de données
|
||||
DB_HOST: db
|
||||
DB_PORT: 3306
|
||||
DB_USER: smartparking_user
|
||||
DB_PASSWORD: smartparking_pass
|
||||
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
|
||||
PORT: 3000
|
||||
networks:
|
||||
- smartparking-network
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
mosquitto_data:
|
||||
mosquitto_log:
|
||||
|
||||
networks:
|
||||
smartparking-network:
|
||||
296
js/map.js
296
js/map.js
@@ -1,110 +1,153 @@
|
||||
/**
|
||||
* ============================================
|
||||
* 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 = {
|
||||
totalSpots: 10, // Nombre total de places
|
||||
updateInterval: 5000 // Intervalle de mise à jour
|
||||
totalSpots: 10,
|
||||
updateInterval: 3000 // Refresh depuis l'API toutes les 3 secondes
|
||||
};
|
||||
|
||||
// État des places
|
||||
let spotsState = {
|
||||
spots: [],
|
||||
selectedSpot: null
|
||||
};
|
||||
|
||||
// Types de places
|
||||
const SPOT_STATUS = {
|
||||
FREE: 'free',
|
||||
OCCUPIED: 'occupied',
|
||||
RESERVED: 'reserved'
|
||||
};
|
||||
const SPOT_STATUS = { FREE: 'free', OCCUPIED: 'occupied', RESERVED: 'reserved' };
|
||||
|
||||
// ============================================
|
||||
// INITIALISATION
|
||||
// ============================================
|
||||
|
||||
// Initialisation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('🗺️ Initialisation de la carte...');
|
||||
initParkingMap();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialise la carte du parking
|
||||
*/
|
||||
function initParkingMap() {
|
||||
// Charger les places depuis le stockage local ou créer les places par défaut
|
||||
loadSpots();
|
||||
async function initParkingMap() {
|
||||
// Essayer d'abord de charger depuis l'API (données MariaDB + Arduino)
|
||||
const loaded = await loadSpotsFromAPI();
|
||||
|
||||
// Si pas d'API disponible, utiliser le localStorage (mode offline)
|
||||
if (!loaded) {
|
||||
loadSpotsFromStorage();
|
||||
}
|
||||
|
||||
// Rendre la carte
|
||||
renderMap();
|
||||
|
||||
// Mettre à jour les statistiques
|
||||
updateStats();
|
||||
|
||||
// Mettre à jour le formulaire de réservation
|
||||
updateReservationForm();
|
||||
|
||||
// Démarrer la simulation (si pas admin)
|
||||
if (!isAdmin()) {
|
||||
startSimulation();
|
||||
// ⭐ Polling toutes les 3 secondes pour recevoir les updates Arduino
|
||||
startAPIPolling();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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');
|
||||
|
||||
if (stored) {
|
||||
spotsState.spots = JSON.parse(stored);
|
||||
} else {
|
||||
// Créer les places par défaut
|
||||
createDefaultSpots();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée les places par défaut
|
||||
*/
|
||||
function createDefaultSpots() {
|
||||
spotsState.spots = [];
|
||||
|
||||
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({
|
||||
id: i,
|
||||
number: i,
|
||||
status: status,
|
||||
status: SPOT_STATUS.FREE,
|
||||
lastUpdate: new Date().toISOString(),
|
||||
sensorId: `SENSOR_${String(i).padStart(3, '0')}`
|
||||
});
|
||||
}
|
||||
|
||||
saveSpots();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde les places
|
||||
*/
|
||||
function saveSpots() {
|
||||
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() {
|
||||
const mapContainer = document.getElementById('parkingMap');
|
||||
if (!mapContainer) return;
|
||||
@@ -114,7 +157,7 @@ function renderMap() {
|
||||
class="parking-spot ${spot.status}"
|
||||
data-id="${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-icon">${getStatusIcon(spot.status)}</span>
|
||||
@@ -122,26 +165,20 @@ function renderMap() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère le clic sur une place
|
||||
*/
|
||||
function handleSpotClick(spotId) {
|
||||
const spot = spotsState.spots.find(s => s.id === spotId);
|
||||
if (!spot) return;
|
||||
|
||||
spotsState.selectedSpot = spot;
|
||||
showSpotDetails(spot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les détails d'une place
|
||||
*/
|
||||
function showSpotDetails(spot) {
|
||||
const container = document.getElementById('spotDetails');
|
||||
if (!container) return;
|
||||
|
||||
const isReserved = spot.status === SPOT_STATUS.RESERVED;
|
||||
const reservation = isReserved ? findReservationForSpot(spot.id) : null;
|
||||
const reservation = spot.status === SPOT_STATUS.RESERVED
|
||||
? findReservationForSpot(spot.id)
|
||||
: null;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="spot-info-detail">
|
||||
@@ -157,7 +194,7 @@ function showSpotDetails(spot) {
|
||||
</div>
|
||||
<div class="spot-info-row">
|
||||
<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 class="spot-info-row">
|
||||
<span class="spot-info-label">Dernière mise à jour</span>
|
||||
@@ -174,7 +211,8 @@ function showSpotDetails(spot) {
|
||||
</div>
|
||||
` : ''}
|
||||
${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>
|
||||
Réserver cette place
|
||||
</button>
|
||||
@@ -183,17 +221,15 @@ function showSpotDetails(spot) {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve la réservation pour une place
|
||||
*/
|
||||
function findReservationForSpot(spotId) {
|
||||
const reservations = JSON.parse(localStorage.getItem('smart_parking_reservations') || '[]');
|
||||
return reservations.find(r => r.spotId === spotId && r.status === 'active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les statistiques
|
||||
*/
|
||||
// ============================================
|
||||
// STATISTIQUES & FORMULAIRE
|
||||
// ============================================
|
||||
|
||||
function updateStats() {
|
||||
const free = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE).length;
|
||||
const occupied = spotsState.spots.filter(s => s.status === SPOT_STATUS.OCCUPIED).length;
|
||||
@@ -205,22 +241,17 @@ function updateStats() {
|
||||
document.getElementById('totalCount').textContent = spotsState.spots.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le formulaire de réservation
|
||||
*/
|
||||
function updateReservationForm() {
|
||||
const select = document.getElementById('resSpot');
|
||||
if (!select) return;
|
||||
|
||||
// Garder la première option
|
||||
const firstOption = select.options[0];
|
||||
select.innerHTML = '';
|
||||
select.appendChild(firstOption);
|
||||
|
||||
// Ajouter uniquement les places libres
|
||||
const freeSpots = spotsState.spots.filter(s => s.status === SPOT_STATUS.FREE);
|
||||
|
||||
freeSpots.forEach(spot => {
|
||||
spotsState.spots
|
||||
.filter(s => s.status === SPOT_STATUS.FREE)
|
||||
.forEach(spot => {
|
||||
const option = document.createElement('option');
|
||||
option.value = spot.id;
|
||||
option.textContent = `Place ${spot.number}`;
|
||||
@@ -228,128 +259,95 @@ function updateReservationForm() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sélectionne une place pour la réservation
|
||||
*/
|
||||
function selectSpotForReservation(spotId) {
|
||||
const select = document.getElementById('resSpot');
|
||||
if (select) {
|
||||
select.value = spotId;
|
||||
}
|
||||
if (select) select.value = spotId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le statut d'une place
|
||||
*/
|
||||
function setSpotStatus(spotId, status) {
|
||||
// ============================================
|
||||
// ACTIONS ADMIN & SIMULATION
|
||||
// ============================================
|
||||
|
||||
async function setSpotStatus(spotId, status) {
|
||||
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();
|
||||
|
||||
// Synchroniser avec l'API si connecté
|
||||
try {
|
||||
const token = localStorage.getItem('smart_parking_token');
|
||||
if (token) {
|
||||
await fetch(`/api/spots/${spot.id}/status`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
}
|
||||
} catch (_err) { /* mode offline */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Change le nombre total de places
|
||||
*/
|
||||
function setTotalSpots(count) {
|
||||
MAP_CONFIG.totalSpots = count;
|
||||
|
||||
// Ajuster le tableau des places
|
||||
if (count > spotsState.spots.length) {
|
||||
// Ajouter des places
|
||||
for (let i = spotsState.spots.length + 1; i <= count; i++) {
|
||||
spotsState.spots.push({
|
||||
id: i,
|
||||
number: i,
|
||||
id: i, number: i,
|
||||
status: SPOT_STATUS.FREE,
|
||||
lastUpdate: new Date().toISOString(),
|
||||
sensorId: `SENSOR_${String(i).padStart(3, '0')}`
|
||||
});
|
||||
}
|
||||
} else if (count < spotsState.spots.length) {
|
||||
// Supprimer des places
|
||||
} else {
|
||||
spotsState.spots = spotsState.spots.slice(0, count);
|
||||
}
|
||||
|
||||
saveSpots();
|
||||
renderMap();
|
||||
updateStats();
|
||||
updateReservationForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulation automatique
|
||||
*/
|
||||
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)];
|
||||
// ============================================
|
||||
// UTILITAIRES
|
||||
// ============================================
|
||||
|
||||
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() {
|
||||
const user = JSON.parse(localStorage.getItem('smart_parking_user') || 'null');
|
||||
return user && user.role === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le label du statut
|
||||
*/
|
||||
function getStatusLabel(status) {
|
||||
const labels = {
|
||||
[SPOT_STATUS.FREE]: 'Libre',
|
||||
[SPOT_STATUS.OCCUPIED]: 'Occupée',
|
||||
[SPOT_STATUS.RESERVED]: 'Réservée'
|
||||
};
|
||||
return labels[status] || 'Inconnu';
|
||||
return { free: 'Libre', occupied: 'Occupée', reserved: 'Réservée' }[status] || 'Inconnu';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'icône du statut
|
||||
*/
|
||||
function getStatusIcon(status) {
|
||||
const icons = {
|
||||
[SPOT_STATUS.FREE]: '✓',
|
||||
[SPOT_STATUS.OCCUPIED]: '🚗',
|
||||
[SPOT_STATUS.RESERVED]: '📅'
|
||||
};
|
||||
return icons[status] || '?';
|
||||
return { free: '✓', occupied: '🚗', reserved: '📅' }[status] || '?';
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('fr-FR', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// Exporter les fonctions
|
||||
// ============================================
|
||||
// EXPORT
|
||||
// ============================================
|
||||
|
||||
window.ParkingMap = {
|
||||
refresh: () => {
|
||||
loadSpots();
|
||||
refresh: async () => {
|
||||
const loaded = await loadSpotsFromAPI();
|
||||
if (!loaded) loadSpotsFromStorage();
|
||||
renderMap();
|
||||
updateStats();
|
||||
updateReservationForm();
|
||||
|
||||
24
mosquitto/config/mosquitto.conf
Normal file
24
mosquitto/config/mosquitto.conf
Normal 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
6
package-lock.json
generated
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "Parking",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* ============================================
|
||||
* DATABASE.JS - Gestion MariaDB
|
||||
* Smart Parking - BTS CIEL IR
|
||||
* MODIFIÉ : ajout de getReservationById
|
||||
* Smart Parking v2.0
|
||||
* AJOUTÉ : expireReservations() — libère auto les places
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,10 @@ const pool = mysql.createPool({
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// INITIALISATION
|
||||
// ============================================
|
||||
|
||||
async function initDatabase() {
|
||||
try {
|
||||
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']);
|
||||
if (rows.length === 0) {
|
||||
const hashed = await bcrypt.hash('admin123', 10);
|
||||
@@ -107,27 +111,23 @@ async function initDatabase() {
|
||||
'INSERT INTO users (name, email, phone, password, role) VALUES (?, ?, ?, ?, ?)',
|
||||
['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');
|
||||
if (spots[0].count === 0) {
|
||||
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(
|
||||
'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) {
|
||||
console.error("❌ Erreur lors de l'initialisation de la base :", err.message);
|
||||
console.error("❌ Erreur init base :", err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -166,8 +166,10 @@ async function getAllUsers() {
|
||||
|
||||
async function updateUser(id, updates) {
|
||||
const fields = Object.keys(updates).map(k => `${k} = ?`).join(', ');
|
||||
const values = Object.values(updates);
|
||||
const [result] = await pool.query(`UPDATE users SET ${fields} WHERE id = ?`, [...values, id]);
|
||||
const [result] = await pool.query(
|
||||
`UPDATE users SET ${fields} WHERE id = ?`,
|
||||
[...Object.values(updates), id]
|
||||
);
|
||||
return { changed: result.affectedRows };
|
||||
}
|
||||
|
||||
@@ -225,10 +227,6 @@ async function createReservation(userId, spotId, date, startTime, endTime, durat
|
||||
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) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT r.*, s.number AS spot_number
|
||||
@@ -271,6 +269,53 @@ async function updateReservationStatus(id, status) {
|
||||
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
|
||||
// ============================================
|
||||
@@ -319,7 +364,7 @@ async function getStats(days = 7) {
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MQTT
|
||||
// MQTT EVENTS
|
||||
// ============================================
|
||||
|
||||
async function recordMqttEvent(topic, message) {
|
||||
@@ -336,11 +381,11 @@ async function recordMqttEvent(topic, message) {
|
||||
|
||||
async function closeDatabase() {
|
||||
await pool.end();
|
||||
console.log('🔌 Connexions à la base fermées');
|
||||
console.log('🔌 Connexions base fermées');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pool, // exposé pour les requêtes directes si besoin
|
||||
pool,
|
||||
initDatabase,
|
||||
closeDatabase,
|
||||
// Utilisateurs
|
||||
@@ -350,6 +395,7 @@ module.exports = {
|
||||
// Réservations
|
||||
createReservation, getReservationById, getReservationsByUser,
|
||||
getAllReservations, updateReservationStatus,
|
||||
expireReservations, // ← NOUVEAU
|
||||
// Historique
|
||||
addHistory, getHistory,
|
||||
// Stats
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "smart-parking-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend Smart Parking avec MariaDB et Docker",
|
||||
"version": "2.0.0",
|
||||
"description": "Backend Smart Parking avec MariaDB, Docker et MQTT Arduino",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"init-db": "node db/init.js"
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
@@ -16,7 +15,8 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.1",
|
||||
"mqtt": "^5.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
|
||||
156
server/server.js
156
server/server.js
@@ -1,47 +1,162 @@
|
||||
/**
|
||||
* ============================================
|
||||
* 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 path = require('path');
|
||||
const mqtt = require('mqtt');
|
||||
require('dotenv').config();
|
||||
|
||||
const db = require('./db/database');
|
||||
const apiRoutes = require('./routes/api');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
app.use(express.static(path.join(__dirname, '..')));
|
||||
|
||||
app.use('/api', apiRoutes);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '..', 'index.html'));
|
||||
app.get('/', (_req, res) => res.sendFile(path.join(__dirname, '..', 'index.html')));
|
||||
app.get('/dashboard', (_req, res) => res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html')));
|
||||
|
||||
// ============================================
|
||||
// CONNEXION MQTT (Mosquitto sur le Raspberry Pi)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Topics attendus depuis l'Arduino :
|
||||
*
|
||||
* smartparking/sensor/1 → "1" = voiture détectée (occupée)
|
||||
* smartparking/sensor/1 → "0" = place libre
|
||||
*
|
||||
* L'Arduino publie sur ce topic via le shield Ethernet/WiFi.
|
||||
* Le numéro à la fin correspond au numéro de place (1 à N).
|
||||
*
|
||||
* Pour tester sans Arduino (ligne de commande sur le Pi) :
|
||||
* 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
|
||||
});
|
||||
|
||||
app.get('/dashboard', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html'));
|
||||
mqttClient.on('connect', () => {
|
||||
console.log('✅ MQTT connecté au broker Mosquitto');
|
||||
mqttClient.subscribe(MQTT_TOPIC, (err) => {
|
||||
if (err) {
|
||||
console.error('❌ Erreur abonnement MQTT :', err.message);
|
||||
} else {
|
||||
console.log(`📡 Abonné au topic : ${MQTT_TOPIC}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Message reçu depuis un capteur Arduino
|
||||
mqttClient.on('message', async (topic, messageBuffer) => {
|
||||
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 - PRÊT POUR DOCKER ║
|
||||
╠══════════════════════════════════════════════════╣
|
||||
║ 🌐 Port : ${PORT}
|
||||
║ 🗄️ Base : MariaDB (${process.env.DB_HOST})
|
||||
╔══════════════════════════════════════════════════════╗
|
||||
║ 🅿️ 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();
|
||||
@@ -55,14 +170,29 @@ async function startServer() {
|
||||
}
|
||||
}, 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 () => {
|
||||
console.log('\n🛑 Arrêt du serveur...');
|
||||
if (mqttClient) mqttClient.end();
|
||||
await db.closeDatabase();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user