Ajout des nouvelles modifications
This commit is contained in:
@@ -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:
|
||||||
|
# Base de données
|
||||||
DB_HOST: db
|
DB_HOST: db
|
||||||
DB_PORT: 3306
|
DB_PORT: 3306
|
||||||
DB_USER: smartparking_user
|
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:
|
||||||
296
js/map.js
296
js/map.js
@@ -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,17 +221,15 @@ 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;
|
||||||
@@ -205,22 +241,17 @@ function updateStats() {
|
|||||||
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}`;
|
||||||
@@ -228,128 +259,95 @@ function updateReservationForm() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.status = status;
|
||||||
spot.lastUpdate = new Date().toISOString();
|
spot.lastUpdate = new Date().toISOString();
|
||||||
saveSpots();
|
saveSpots();
|
||||||
renderMap();
|
renderMap();
|
||||||
updateStats();
|
updateStats();
|
||||||
updateReservationForm();
|
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) {
|
function setTotalSpots(count) {
|
||||||
MAP_CONFIG.totalSpots = count;
|
MAP_CONFIG.totalSpots = count;
|
||||||
|
|
||||||
// Ajuster le tableau des places
|
|
||||||
if (count > spotsState.spots.length) {
|
if (count > spotsState.spots.length) {
|
||||||
// Ajouter des places
|
|
||||||
for (let i = spotsState.spots.length + 1; i <= count; i++) {
|
for (let i = spotsState.spots.length + 1; i <= count; i++) {
|
||||||
spotsState.spots.push({
|
spotsState.spots.push({
|
||||||
id: i,
|
id: i, number: i,
|
||||||
number: i,
|
|
||||||
status: SPOT_STATUS.FREE,
|
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')}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (count < spotsState.spots.length) {
|
} else {
|
||||||
// Supprimer des places
|
|
||||||
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();
|
||||||
|
|||||||
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
|
* DATABASE.JS - Gestion MariaDB
|
||||||
* Smart Parking - BTS CIEL IR
|
* Smart Parking v2.0
|
||||||
* MODIFIÉ : ajout de getReservationById
|
* AJOUTÉ : expireReservations() — libère auto les places
|
||||||
* ============================================
|
* ============================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -21,6 +21,10 @@ const pool = mysql.createPool({
|
|||||||
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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
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 express = require('express');
|
||||||
const cors = require('cors');
|
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')));
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 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) => {
|
mqttClient.on('connect', () => {
|
||||||
res.sendFile(path.join(__dirname, '..', 'pages', 'dashboard.html'));
|
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() {
|
async function startServer() {
|
||||||
try {
|
try {
|
||||||
|
// 1. Initialiser la base de données
|
||||||
await db.initDatabase();
|
await db.initDatabase();
|
||||||
|
|
||||||
|
// 2. Démarrer le serveur HTTP
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`
|
console.log(`
|
||||||
╔══════════════════════════════════════════════════╗
|
╔══════════════════════════════════════════════════════╗
|
||||||
║ 🅿️ SMART PARKING SERVER - PRÊT POUR DOCKER ║
|
║ 🅿️ SMART PARKING SERVER v2.0 - PRÊT ║
|
||||||
╠══════════════════════════════════════════════════╣
|
╠══════════════════════════════════════════════════════╣
|
||||||
║ 🌐 Port : ${PORT}
|
║ 🌐 Port HTTP : ${PORT}
|
||||||
║ 🗄️ Base : MariaDB (${process.env.DB_HOST})
|
║ 🗄️ Base : MariaDB (${process.env.DB_HOST || 'localhost'})
|
||||||
|
║ 📡 MQTT : ${MQTT_HOST}:${MQTT_PORT}
|
||||||
║ 🔐 JWT sécurisé
|
║ 🔐 JWT sécurisé
|
||||||
╚══════════════════════════════════════════════════╝
|
╚══════════════════════════════════════════════════════╝
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3. Connecter le client MQTT (broker Mosquitto)
|
||||||
|
connectMQTT();
|
||||||
|
|
||||||
|
// 4. Enregistrer les statistiques toutes les 5 minutes
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const spots = await db.getAllSpots();
|
const spots = await db.getAllSpots();
|
||||||
@@ -55,14 +170,29 @@ async function startServer() {
|
|||||||
}
|
}
|
||||||
}, 5 * 60 * 1000);
|
}, 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) {
|
} catch (err) {
|
||||||
console.error('❌ Erreur au démarrage :', err);
|
console.error('❌ Erreur au démarrage :', err);
|
||||||
process.exit(1);
|
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...');
|
||||||
|
if (mqttClient) mqttClient.end();
|
||||||
await db.closeDatabase();
|
await db.closeDatabase();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user