|
|
|
|
@ -0,0 +1,471 @@
@@ -0,0 +1,471 @@
|
|
|
|
|
<!DOCTYPE html> |
|
|
|
|
<html lang="fr"> |
|
|
|
|
<head> |
|
|
|
|
<meta charset="UTF-8"> |
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
|
|
<title>ESP32 Bluetooth Monitor</title> |
|
|
|
|
<style> |
|
|
|
|
* { |
|
|
|
|
margin: 0; |
|
|
|
|
padding: 0; |
|
|
|
|
box-sizing: border-box; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
body { |
|
|
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
|
|
min-height: 100vh; |
|
|
|
|
padding: 20px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.container { |
|
|
|
|
max-width: 1200px; |
|
|
|
|
margin: 0 auto; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
h1 { |
|
|
|
|
color: white; |
|
|
|
|
text-align: center; |
|
|
|
|
margin-bottom: 30px; |
|
|
|
|
font-size: 2.5em; |
|
|
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.controls { |
|
|
|
|
background: white; |
|
|
|
|
padding: 20px; |
|
|
|
|
border-radius: 15px; |
|
|
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
|
|
|
|
margin-bottom: 30px; |
|
|
|
|
display: flex; |
|
|
|
|
gap: 15px; |
|
|
|
|
align-items: center; |
|
|
|
|
flex-wrap: wrap; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
button { |
|
|
|
|
padding: 12px 24px; |
|
|
|
|
font-size: 16px; |
|
|
|
|
border: none; |
|
|
|
|
border-radius: 8px; |
|
|
|
|
cursor: pointer; |
|
|
|
|
transition: all 0.3s ease; |
|
|
|
|
font-weight: 600; |
|
|
|
|
text-transform: uppercase; |
|
|
|
|
letter-spacing: 0.5px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#connectBtn { |
|
|
|
|
background: #4CAF50; |
|
|
|
|
color: white; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#connectBtn:hover { |
|
|
|
|
background: #45a049; |
|
|
|
|
transform: translateY(-2px); |
|
|
|
|
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#connectBtn:disabled { |
|
|
|
|
background: #cccccc; |
|
|
|
|
cursor: not-allowed; |
|
|
|
|
transform: none; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.status { |
|
|
|
|
flex: 1; |
|
|
|
|
text-align: right; |
|
|
|
|
font-weight: 600; |
|
|
|
|
color: #666; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.modules-grid { |
|
|
|
|
display: grid; |
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); |
|
|
|
|
gap: 20px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.module-card { |
|
|
|
|
background: white; |
|
|
|
|
border-radius: 15px; |
|
|
|
|
padding: 25px; |
|
|
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
|
|
|
|
transition: transform 0.3s ease; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.module-card:hover { |
|
|
|
|
transform: translateY(-5px); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.module-header { |
|
|
|
|
display: flex; |
|
|
|
|
justify-content: space-between; |
|
|
|
|
align-items: center; |
|
|
|
|
margin-bottom: 20px; |
|
|
|
|
padding-bottom: 15px; |
|
|
|
|
border-bottom: 3px solid #667eea; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.module-name { |
|
|
|
|
font-size: 1.5em; |
|
|
|
|
font-weight: bold; |
|
|
|
|
color: #667eea; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.disconnect-btn { |
|
|
|
|
background: #f44336; |
|
|
|
|
color: white; |
|
|
|
|
padding: 8px 16px; |
|
|
|
|
font-size: 14px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.disconnect-btn:hover { |
|
|
|
|
background: #da190b; |
|
|
|
|
transform: translateY(-2px); |
|
|
|
|
box-shadow: 0 5px 15px rgba(244, 67, 54, 0.4); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.gpio-list { |
|
|
|
|
display: flex; |
|
|
|
|
flex-direction: column; |
|
|
|
|
gap: 12px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.gpio-item { |
|
|
|
|
display: flex; |
|
|
|
|
justify-content: space-between; |
|
|
|
|
align-items: center; |
|
|
|
|
padding: 15px; |
|
|
|
|
background: #f5f5f5; |
|
|
|
|
border-radius: 10px; |
|
|
|
|
transition: all 0.3s ease; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.gpio-item.active { |
|
|
|
|
background: #ffebee; |
|
|
|
|
border-left: 5px solid #f44336; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.gpio-label { |
|
|
|
|
font-weight: 600; |
|
|
|
|
color: #333; |
|
|
|
|
font-size: 1.1em; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.gpio-status { |
|
|
|
|
padding: 8px 20px; |
|
|
|
|
border-radius: 20px; |
|
|
|
|
font-weight: bold; |
|
|
|
|
font-size: 0.9em; |
|
|
|
|
text-transform: uppercase; |
|
|
|
|
letter-spacing: 1px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.gpio-status.low { |
|
|
|
|
background: #f44336; |
|
|
|
|
color: white; |
|
|
|
|
animation: pulse 1.5s infinite; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.gpio-status.high { |
|
|
|
|
background: #4CAF50; |
|
|
|
|
color: white; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@keyframes pulse { |
|
|
|
|
0%, 100% { |
|
|
|
|
opacity: 1; |
|
|
|
|
} |
|
|
|
|
50% { |
|
|
|
|
opacity: 0.7; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.empty-state { |
|
|
|
|
text-align: center; |
|
|
|
|
padding: 60px 20px; |
|
|
|
|
color: white; |
|
|
|
|
font-size: 1.2em; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.empty-state-icon { |
|
|
|
|
font-size: 4em; |
|
|
|
|
margin-bottom: 20px; |
|
|
|
|
opacity: 0.7; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.notification { |
|
|
|
|
position: fixed; |
|
|
|
|
top: 20px; |
|
|
|
|
right: 20px; |
|
|
|
|
background: white; |
|
|
|
|
padding: 15px 25px; |
|
|
|
|
border-radius: 10px; |
|
|
|
|
box-shadow: 0 5px 20px rgba(0,0,0,0.3); |
|
|
|
|
z-index: 1000; |
|
|
|
|
animation: slideIn 0.3s ease; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@keyframes slideIn { |
|
|
|
|
from { |
|
|
|
|
transform: translateX(400px); |
|
|
|
|
opacity: 0; |
|
|
|
|
} |
|
|
|
|
to { |
|
|
|
|
transform: translateX(0); |
|
|
|
|
opacity: 1; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.last-update { |
|
|
|
|
color: #999; |
|
|
|
|
font-size: 0.85em; |
|
|
|
|
margin-top: 10px; |
|
|
|
|
text-align: right; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
</head> |
|
|
|
|
<body> |
|
|
|
|
<div class="container"> |
|
|
|
|
<h1>🔵 ESP32 Bluetooth Monitor</h1> |
|
|
|
|
|
|
|
|
|
<div class="controls"> |
|
|
|
|
<button id="connectBtn">Connecter un module</button> |
|
|
|
|
<div class="status" id="status">Aucun module connecté</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div class="modules-grid" id="modulesContainer"> |
|
|
|
|
<div class="empty-state"> |
|
|
|
|
<div class="empty-state-icon">📡</div> |
|
|
|
|
<div>Cliquez sur "Connecter un module" pour ajouter un ESP32</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<script> |
|
|
|
|
// Configuration BLE |
|
|
|
|
const SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b'; |
|
|
|
|
const CHARACTERISTIC_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26a8'; |
|
|
|
|
|
|
|
|
|
// Stockage des modules connectés |
|
|
|
|
let connectedModules = new Map(); |
|
|
|
|
|
|
|
|
|
// Éléments DOM |
|
|
|
|
const connectBtn = document.getElementById('connectBtn'); |
|
|
|
|
const statusDiv = document.getElementById('status'); |
|
|
|
|
const modulesContainer = document.getElementById('modulesContainer'); |
|
|
|
|
|
|
|
|
|
// Vérifier si Web Bluetooth est disponible |
|
|
|
|
if (!navigator.bluetooth) { |
|
|
|
|
alert('Web Bluetooth n\'est pas supporté par votre navigateur. Utilisez Chrome, Edge ou Opera.'); |
|
|
|
|
connectBtn.disabled = true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fonction pour afficher une notification |
|
|
|
|
function showNotification(message) { |
|
|
|
|
const notification = document.createElement('div'); |
|
|
|
|
notification.className = 'notification'; |
|
|
|
|
notification.textContent = message; |
|
|
|
|
document.body.appendChild(notification); |
|
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
notification.remove(); |
|
|
|
|
}, 3000); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fonction pour mettre à jour le statut |
|
|
|
|
function updateStatus() { |
|
|
|
|
const count = connectedModules.size; |
|
|
|
|
statusDiv.textContent = count === 0 |
|
|
|
|
? 'Aucun module connecté' |
|
|
|
|
: `${count} module${count > 1 ? 's' : ''} connecté${count > 1 ? 's' : ''}`; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fonction pour créer la carte d'un module |
|
|
|
|
function createModuleCard(moduleName, device) { |
|
|
|
|
const card = document.createElement('div'); |
|
|
|
|
card.className = 'module-card'; |
|
|
|
|
card.id = `module-${moduleName}`; |
|
|
|
|
|
|
|
|
|
card.innerHTML = ` |
|
|
|
|
<div class="module-header"> |
|
|
|
|
<div class="module-name">${moduleName}</div> |
|
|
|
|
<button class="disconnect-btn" onclick="disconnectModule('${moduleName}')"> |
|
|
|
|
Déconnecter |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
<div class="gpio-list" id="gpio-list-${moduleName}"> |
|
|
|
|
<div style="text-align: center; color: #999;">En attente de données...</div> |
|
|
|
|
</div> |
|
|
|
|
<div class="last-update" id="last-update-${moduleName}"> |
|
|
|
|
Dernière mise à jour: -- |
|
|
|
|
</div> |
|
|
|
|
`; |
|
|
|
|
|
|
|
|
|
return card; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fonction pour mettre à jour l'affichage des GPIO |
|
|
|
|
function updateGPIODisplay(moduleName, gpioData) { |
|
|
|
|
console.log(`[updateGPIODisplay] Module: ${moduleName}`, gpioData); |
|
|
|
|
const gpioList = document.getElementById(`gpio-list-${moduleName}`); |
|
|
|
|
const lastUpdate = document.getElementById(`last-update-${moduleName}`); |
|
|
|
|
|
|
|
|
|
if (!gpioList) { |
|
|
|
|
console.error(`[updateGPIODisplay] Élément gpio-list non trouvé pour ${moduleName}`); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
gpioList.innerHTML = ''; |
|
|
|
|
|
|
|
|
|
gpioData.forEach(gpio => { |
|
|
|
|
// Convertir en string pour comparaison cohérente |
|
|
|
|
const stateStr = String(gpio.state); |
|
|
|
|
const isLow = (stateStr === '1' || stateStr === 1); |
|
|
|
|
|
|
|
|
|
console.log(` GPIO ${gpio.pin}: state=${gpio.state} (${typeof gpio.state}), isLow=${isLow}`); |
|
|
|
|
|
|
|
|
|
const gpioItem = document.createElement('div'); |
|
|
|
|
gpioItem.className = `gpio-item ${isLow ? 'active' : ''}`; |
|
|
|
|
|
|
|
|
|
gpioItem.innerHTML = ` |
|
|
|
|
<div class="gpio-label">GPIO ${gpio.pin}</div> |
|
|
|
|
<div class="gpio-status ${isLow ? 'low' : 'high'}"> |
|
|
|
|
${isLow ? 'BAS' : 'HAUT'} |
|
|
|
|
</div> |
|
|
|
|
`; |
|
|
|
|
|
|
|
|
|
gpioList.appendChild(gpioItem); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Mettre à jour l'horodatage |
|
|
|
|
const now = new Date(); |
|
|
|
|
lastUpdate.textContent = `Dernière mise à jour: ${now.toLocaleTimeString()}`; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fonction pour connecter un module |
|
|
|
|
async function connectModule() { |
|
|
|
|
try { |
|
|
|
|
console.log('Recherche d\'un appareil Bluetooth...'); |
|
|
|
|
|
|
|
|
|
const device = await navigator.bluetooth.requestDevice({ |
|
|
|
|
filters: [{ services: [SERVICE_UUID] }] |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
console.log('Appareil trouvé:', device.name); |
|
|
|
|
showNotification(`Connexion à ${device.name}...`); |
|
|
|
|
|
|
|
|
|
const server = await device.gatt.connect(); |
|
|
|
|
console.log('Connecté au serveur GATT'); |
|
|
|
|
|
|
|
|
|
const service = await server.getPrimaryService(SERVICE_UUID); |
|
|
|
|
const characteristic = await service.getCharacteristic(CHARACTERISTIC_UUID); |
|
|
|
|
|
|
|
|
|
// Lire l'état initial immédiatement |
|
|
|
|
const initialValue = await characteristic.readValue(); |
|
|
|
|
const initialData = new TextDecoder().decode(initialValue); |
|
|
|
|
console.log('État initial:', initialData); |
|
|
|
|
|
|
|
|
|
// Traiter l'état initial |
|
|
|
|
try { |
|
|
|
|
const data = JSON.parse(initialData); |
|
|
|
|
const moduleName = data.module; |
|
|
|
|
|
|
|
|
|
// Créer la carte du module |
|
|
|
|
const emptyState = modulesContainer.querySelector('.empty-state'); |
|
|
|
|
if (emptyState) emptyState.remove(); |
|
|
|
|
|
|
|
|
|
const card = createModuleCard(moduleName, device); |
|
|
|
|
modulesContainer.appendChild(card); |
|
|
|
|
|
|
|
|
|
// Stocker les informations |
|
|
|
|
connectedModules.set(moduleName, { |
|
|
|
|
device: device, |
|
|
|
|
characteristic: characteristic, |
|
|
|
|
gpios: data.gpios |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Afficher l'état initial |
|
|
|
|
updateGPIODisplay(moduleName, data.gpios); |
|
|
|
|
updateStatus(); |
|
|
|
|
showNotification(`${moduleName} connecté avec succès!`); |
|
|
|
|
} catch (e) { |
|
|
|
|
console.error('Erreur parsing état initial:', e); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Activer les notifications pour les changements futurs |
|
|
|
|
await characteristic.startNotifications(); |
|
|
|
|
console.log('Notifications activées'); |
|
|
|
|
|
|
|
|
|
// Écouter les notifications |
|
|
|
|
characteristic.addEventListener('characteristicvaluechanged', (event) => { |
|
|
|
|
const value = new TextDecoder().decode(event.target.value); |
|
|
|
|
console.log('🔔 [Notification reçue]:', value); |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const data = JSON.parse(value); |
|
|
|
|
const moduleName = data.module; |
|
|
|
|
console.log(`📦 Données parsées:`, data); |
|
|
|
|
|
|
|
|
|
// Mettre à jour les informations du module |
|
|
|
|
if (connectedModules.has(moduleName)) { |
|
|
|
|
console.log(`✅ Module ${moduleName} trouvé, mise à jour...`); |
|
|
|
|
connectedModules.get(moduleName).gpios = data.gpios; |
|
|
|
|
// Mettre à jour l'affichage |
|
|
|
|
updateGPIODisplay(moduleName, data.gpios); |
|
|
|
|
} else { |
|
|
|
|
console.warn(`⚠️ Module ${moduleName} non trouvé dans connectedModules`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} catch (e) { |
|
|
|
|
console.error('❌ Erreur lors du parsing JSON:', e); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Gérer la déconnexion |
|
|
|
|
device.addEventListener('gattserverdisconnected', () => { |
|
|
|
|
console.log(`${device.name} déconnecté`); |
|
|
|
|
showNotification(`${device.name} déconnecté`); |
|
|
|
|
|
|
|
|
|
// Trouver et supprimer le module |
|
|
|
|
for (let [name, module] of connectedModules.entries()) { |
|
|
|
|
if (module.device === device) { |
|
|
|
|
connectedModules.delete(name); |
|
|
|
|
const card = document.getElementById(`module-${name}`); |
|
|
|
|
if (card) card.remove(); |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
updateStatus(); |
|
|
|
|
|
|
|
|
|
// Afficher l'état vide si aucun module |
|
|
|
|
if (connectedModules.size === 0) { |
|
|
|
|
modulesContainer.innerHTML = ` |
|
|
|
|
<div class="empty-state"> |
|
|
|
|
<div class="empty-state-icon">📡</div> |
|
|
|
|
<div>Cliquez sur "Connecter un module" pour ajouter un ESP32</div> |
|
|
|
|
</div> |
|
|
|
|
`; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
console.error('Erreur de connexion:', error); |
|
|
|
|
showNotification(`Erreur: ${error.message}`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Fonction pour déconnecter un module |
|
|
|
|
window.disconnectModule = function(moduleName) { |
|
|
|
|
const module = connectedModules.get(moduleName); |
|
|
|
|
if (module && module.device.gatt.connected) { |
|
|
|
|
module.device.gatt.disconnect(); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// Événement du bouton de connexion |
|
|
|
|
connectBtn.addEventListener('click', connectModule); |
|
|
|
|
</script> |
|
|
|
|
</body> |
|
|
|
|
</html> |