Skip to main content

Live Data Logger

Ce projet permet de visualiser en temps réel les données environnementales (Température, Humidité, Pression par exemple) et le niveau de batterie d'un montage ESP32, directement dans un navigateur web, sans passer par un serveur cloud ni une base de données externe.

CleanShot 2026-01-12 at 18.12.52@2x.png

🌟 Fonctionnalités

  • Serveur Web autonome hébergé sur l'ESP32.

  • Visualisation Live via un graphique dynamique (Chart.js).

  • Technologie WebSocket pour une transmission fluide et rapide des données.

  • Interface "Dark Mode" avec indicateur de batterie intégré.

  • Export CSV des données acquises depuis le navigateur.


🛠️ Matériel Requis

  • Microcontrôleur : ESP32 (DevKit V1 ou équivalent).

  • Capteur Environnement : BME280 (I2C).

  • Gestion Batterie : Adafruit MAX17048 (ou compatible) LiPoly / LiIon Fuel Gauge.

  • Batterie : LiPo 3.7V.

Câblage (I2C)

Les deux capteurs utilisent le bus I2C. Ils peuvent être connectés en parallèle sur les mêmes broches de l'ESP32.

Capteur (Pin) ESP32 (Pin)
VCC 3.3V
GND GND
SDA GPIO 21 (D21)
SCL GPIO 22 (D22)

Note : Vérifiez l'adresse I2C du BME280. Le code cherche par défaut l'adresse 0x77 ou 0x76.


💻 Installation Logicielle

1. Bibliothèques Arduino

Installez les bibliothèques suivantes via le Gestionnaire de bibliothèques de l'IDE Arduino :

  • Adafruit BME280 Library

  • Adafruit MAX1704X

  • Adafruit Unified Sensor

2. Bibliothèques Web (ESP32)

Ces bibliothèques doivent souvent être installées manuellement (téléchargement ZIP) si elles ne sont pas dans le gestionnaire :


📂 Le Code Source

Le projet se compose de deux fichiers à placer dans le même dossier de sketch Arduino.

1. Fichier webpage.h

Ce fichier contient l'interface HTML, le CSS (style) et le JavaScript (logique client).

Créez un nouvel onglet dans l'IDE Arduino nommé webpage.h et collez ceci :

C++

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP32 Multi-Stream</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    body { 
      background-color: #0b0c10; color: #66fcf1; 
      font-family: 'Courier New', Courier, monospace; 
      text-align: center; margin: 0; padding: 10px;
    }
    .header-container { display: flex; justify-content: space-between; align-items: center; padding: 0 10px; margin-bottom: 10px; }
    h2 { margin: 0; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 10px rgba(102, 252, 241, 0.5); font-size: 1.2rem; }
    .battery-wrapper { display: flex; align-items: center; gap: 8px; }
    .battery-icon { width: 40px; height: 20px; border: 2px solid #66fcf1; border-radius: 4px; position: relative; padding: 2px; }
    .battery-icon::after { content: ''; position: absolute; right: -5px; top: 4px; width: 3px; height: 8px; background: #66fcf1; border-radius: 0 2px 2px 0; }
    .battery-level { height: 100%; width: 100%; background: #66fcf1; border-radius: 2px; transition: width 0.5s, background-color 0.5s; }
    #bat-text { font-weight: bold; font-size: 0.9rem; }
    .card { background: #1f2833; border: 1px solid #45a29e; box-shadow: 0 0 20px rgba(69, 162, 158, 0.2); border-radius: 8px; padding: 10px; margin: 15px auto; height: 60vh; width: 95vw; }
    #chart-container { position: relative; height: 100%; width: 100%; }
    #values-panel { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; margin: 20px auto; }
    .val-box { border: 1px solid #45a29e; border-radius: 5px; padding: 10px 20px; min-width: 100px; background: #0b0c10; }
    .val-label { font-size: 0.7em; color: #c5c6c7; display: block; }
    .val-number { font-size: 1.4em; font-weight: bold; }
    button { background: transparent; border: 1px solid #66fcf1; color: #66fcf1; padding: 10px 20px; font-size: 14px; margin: 10px; cursor: pointer; text-transform: uppercase; transition: 0.3s; }
    button:hover { background: #66fcf1; color: #0b0c10; box-shadow: 0 0 15px #66fcf1; }
    button:disabled { border-color: #444; color: #444; cursor: not-allowed; box-shadow: none; }
    #btn-stop:hover { background: #ff0055; border-color: #ff0055; box-shadow: 0 0 15px #ff0055; color: white; }
  </style>
</head>
<body>
  <div class="header-container">
    <h2>Monitoring</h2>
    <div class="battery-wrapper">
      <span id="bat-text">--%</span>
      <div class="battery-icon"><div id="bat-level" class="battery-level"></div></div>
    </div>
  </div>
  <div id="values-panel"><span style="color:#888">Connexion...</span></div>
  <div class="card"><div id="chart-container"><canvas id="sensorChart"></canvas></div></div>
  <div class="controls">
    <button id="btn-stop" onclick="stopLogging()">STOP</button>
    <button id="btn-save" onclick="downloadData()" disabled>CSV</button>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  var chart;
  var dataLog = []; 
  var isLogging = true;
  var sensorConfig = [];

  function initChart() {
    var ctx = document.getElementById('sensorChart').getContext('2d');
    chart = new Chart(ctx, {
        type: 'line',
        data: { labels: [], datasets: [] },
        options: { 
          animation: false, responsive: true, maintainAspectRatio: false,
          interaction: { mode: 'index', intersect: false },
          scales: { x: { ticks: { color: '#888' }, grid: { color: '#333' } }, y: { ticks: { color: '#66fcf1' }, grid: { color: '#333' } } },
          plugins: { legend: { labels: { color: '#fff' } } }
        }
    });
  }

  function initWebSocket() {
    websocket = new WebSocket(gateway);
    websocket.onopen = () => console.log('WS Connecté');
    websocket.onclose = () => setTimeout(initWebSocket, 2000);
    websocket.onmessage = onMessage;
  }

  function onMessage(event) {
    if (!isLogging) return;
    var msg = event.data;
    if (msg.indexOf("config") > 0) { setupInterface(JSON.parse(msg)); return; }
    try {
        var data = JSON.parse(msg);
        var t = new Date().toLocaleTimeString();
        var values = data.v; 
        var battery = data.b;
        if(battery !== undefined) {
           document.getElementById('bat-text').innerText = battery + "%";
           var bar = document.getElementById('bat-level');
           bar.style.width = battery + "%";
           if(battery > 50) bar.style.backgroundColor = "#66fcf1";
           else if(battery > 20) bar.style.backgroundColor = "#ffcc00";
           else bar.style.backgroundColor = "#ff3333";
        }
        values.forEach((val, index) => { var el = document.getElementById(`val-${index}`); if(el) el.innerText = val.toFixed(1); });
        dataLog.push({time: t, vals: values});
        chart.data.labels.push(t);
        chart.data.datasets.forEach((dataset, index) => { if(values[index] !== undefined) dataset.data.push(values[index]); });
        chart.update();
    } catch(e) { console.error("Erreur Parsing", e); }
  }

  function setupInterface(cfg) {
    sensorConfig = cfg.sensors;
    chart.options.scales.y.min = cfg.min;
    chart.options.scales.y.max = cfg.max;
    chart.data.datasets = sensorConfig.map(s => ({
        label: s.name, borderColor: s.color, backgroundColor: s.color,
        borderWidth: 2, pointRadius: 0, tension: 0.3, fill: false, data: []
    }));
    chart.update();
    var panel = document.getElementById("values-panel");
    panel.innerHTML = "";
    sensorConfig.forEach((s, index) => {
        panel.innerHTML += `<div class="val-box" style="border-color:${s.color}"><span class="val-label" style="color:${s.color}">${s.name}</span><span id="val-${index}" class="val-number">--</span></div>`;
    });
  }

  function stopLogging() {
    isLogging = false;
    websocket.close();
    document.getElementById('btn-stop').style.display = 'none';
    document.getElementById('btn-save').disabled = false;
  }

  function downloadData() {
    let header = "Time"; sensorConfig.forEach(s => header += "," + s.name);
    let csv = header + "\n";
    dataLog.forEach(row => { csv += row.time; row.vals.forEach(val => csv += "," + val); csv += "\n"; });
    var link = document.createElement("a"); link.href = "data:text/csv;charset=utf-8," + encodeURI(csv); link.download = "data.csv"; link.click();
  }

  window.onload = () => { initChart(); initWebSocket(); };
</script>
</body>
</html>
)rawliteral";

2. Programme Principal (.ino)

C'est le code qui gère les capteurs et le serveur. Assurez-vous de modifier le SSID et le MOT DE PASSE.

C++

// =================================
// --- INCLUDE CAPTEURS ---
// =================================
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
Adafruit_BME280 bme; // I2C

#include <Adafruit_MAX1704X.h>
Adafruit_MAX17048 maxlipo;

// =================================
// --- Include WiFi / serveur ---
// =================================
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "webpage.h"

// =================================
// --- CONFIGURATION UTILISATEUR ---
// =================================
const char* ssid = "SSID";
const char* password = "MDP";

// Définissez ici l'échelle fixe de votre graphique
const int Y_MIN = 0;   
const int Y_MAX = 100;

// DEFINITION DES COURBES (Nom, Couleur Hexa)
struct Sensor {
  String name;
  String color;
};

Sensor sensors[] = {
  { "Temperature", "#ff3333" },  // Rouge
  { "Humidité",    "#33ff33" },  // Vert Néon
  { "Pression",    "#3388ff" }   // Bleu
};

// Calcul automatique du nombre de capteurs
const int SENSOR_COUNT = sizeof(sensors) / sizeof(sensors[0]);

// Vitesse d'envoi (ms)
const int SEND_INTERVAL = 1000;
// =================================


// INITIALISATION SERVEUR & WEBSOCKET
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

int sensorValue = 0;

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  if (type == WS_EVT_CONNECT) {
    Serial.println("Client connecté ! Envoi de la config...");
    // Envoi de la configuration JSON (échelle, noms des capteurs, couleurs)
    String json = "{\"type\":\"config\",\"min\":" + String(Y_MIN) + ",\"max\":" + String(Y_MAX) + ",\"sensors\":[";
    for (int i = 0; i < SENSOR_COUNT; i++) {
      json += "{\"name\":\"" + sensors[i].name + "\",\"color\":\"" + sensors[i].color + "\"}";
      if (i < SENSOR_COUNT - 1) json += ",";
    }
    json += "]}";
    client->text(json);
  }
}

void setup() {
  Serial.begin(9600);
  
  // --- SETUP MAX17048 (Batterie) ---
  while (!maxlipo.begin()) {
    Serial.println(F("Couldnt find Adafruit MAX17048?\nMake sure a battery is plugged in!"));
    delay(2000);
  }

  // --- SETUP BME280 (Environnement) ---
  unsigned status = bme.begin();  
  if (!status) {
      Serial.println("Erreur BME280 ! Vérifiez le câblage.");
      while (1) delay(10);
  }

  // Configuration en mode "Forced" pour économiser l'énergie
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                Adafruit_BME280::SAMPLING_X1, // Température
                Adafruit_BME280::SAMPLING_X1, // Pression
                Adafruit_BME280::SAMPLING_X1, // Humidité
                Adafruit_BME280::FILTER_OFF   );
  
  // --- WIFI ---
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connexion au WiFi...");
  }
  Serial.println(WiFi.localIP());

  // --- SERVEUR ---
  ws.onEvent(onEvent);
  server.addHandler(&ws);

  server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(204); });
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html); });
  
  server.begin();
}

void loop() {
  // Lecture des capteurs
  bme.takeForcedMeasurement(); // Demande une nouvelle mesure

  float readings[SENSOR_COUNT];
  readings[0] = bme.readTemperature();            // Temp
  readings[1] = bme.readHumidity();               // Hum
  readings[2] = bme.readPressure() / 100.0F;      // Pression (hPa)

  ws.cleanupClients(); 
  
  if (ws.count() > 0) {
    // Construction JSON : {"v": [val1, val2, val3], "b": 85.5}
    String json = "{\"v\":[";
    for(int i=0; i<SENSOR_COUNT; i++){
      json += String(readings[i]);
      if(i < SENSOR_COUNT -1) json += ",";
    }
    json += "], \"b\":" + String(maxlipo.cellPercent()) + "}";
    
    ws.textAll(json);
  }
  
  delay(SEND_INTERVAL);
}


🚀 Utilisation

  1. Téléversez le code sur l'ESP32.

  2. Ouvrez le Moniteur Série (Baudrate 9600) pour voir l'adresse IP (ex: 192.168.1.45).

  3. Connectez votre ordinateur ou téléphone au même réseau WiFi.

  4. Ouvrez un navigateur et tapez l'adresse IP.

  5. Le tableau de bord s'affiche : les courbes se dessinent et le niveau de batterie apparait en haut à droite.

  6. Cliquez sur STOP puis CSV pour récupérer vos données.