BUT Mesures Physiques · IUT Aix-Marseille · 2026-2027

Exemple : la page web des mesures du capteur

Un seul fichier index.html : il demande les mesures du groupe au serveur AirCarto et les trace avec Chart.js (particules, température et humidité, CO2), avec un choix de période. À ouvrir d'un double-clic pour tester, puis à publier sur GitHub Pages.

Code de l'exemple du guide IUT-AMU-11 « Une page web pour montrer les mesures de votre capteur », v1.0 du 2026-11-02. Les explications sont dans le PDF.

Mise en route

  1. Téléchargez le projet (bouton vert) et décompressez-le, ou copiez index.html ci-dessous avec le bouton Copier dans un fichier du même nom.
  2. Dans index.html, remplacez VOTRE_CLE par la clé du groupe.
  3. Double-cliquez sur le fichier : la page s'ouvre dans le navigateur avec vos mesures. F12, onglet Console, si rien ne s'affiche.
  4. Publiez-la sur GitHub Pages : dépôt public, Upload files, puis Settings › Pages, branche main (guide, partie 4).

La clé du groupe sera lisible par tous dans la page publiée : c'est accepté pour ce TP, mais quelqu'un pourrait s'en servir pour envoyer des mesures. Valeurs étranges que votre carte n'a pas envoyées : prévenez l'enseignant, il vous donnera une nouvelle clé. Jamais de mot de passe ENT dans la page.

Le projet

Télécharger le projet IUT-AMU-11-exemple-v1.0.zip
index.html
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notre capteur</title>
<!-- Chart.js : la bibliothèque qui dessine les graphes, chargée depuis un CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>
  body {
    font-family: system-ui, sans-serif; color: #1b2420; background: #f6f7f6;
    max-width: 900px; margin: 0 auto; padding: 16px;
  }
  h1 { margin-bottom: 4px; }
  .carte {
    background: #fff; border: 1px solid #d6ddd8; border-radius: 8px;
    padding: 12px 16px; margin: 16px 0;
  }
  #valeurs { display: flex; flex-wrap: wrap; gap: 24px; }
  .valeur b { display: block; font-size: 1.8rem; }
  #etat { color: #5a6a60; }
  #etat.erreur { color: #b3261e; }
</style>
</head>
<body>

<h1>Notre capteur</h1>
<p>Mesures envoyées par la carte du groupe au serveur AirCarto.</p>

<p>
  Période :
  <select id="periode">
    <option value="-6h">6 dernières heures</option>
    <option value="-24h" selected>24 dernières heures</option>
    <option value="-7d">7 derniers jours</option>
  </select>
</p>
<p id="etat">Chargement…</p>

<div class="carte">
  <h2>Dernière mesure</h2>
  <div id="valeurs"></div>
</div>

<div class="carte">
  <h2>Particules (µg/m³)</h2>
  <canvas id="graphe-pm"></canvas>
</div>
<div class="carte">
  <h2>Température (°C) et humidité (%)</h2>
  <canvas id="graphe-meteo"></canvas>
</div>
<div class="carte" id="carte-co2">
  <h2>CO₂ (ppm)</h2>
  <canvas id="graphe-co2"></canvas>
</div>

<script>
// ---------- Réglages : à remplacer ----------
const CLE = "VOTRE_CLE";   // clé du groupe (32 caractères)
const DEVICE_ID = "";      // adresse MAC d'une carte, ou "" pour toutes

// Une moyenne par intervalle selon la période : assez de points pour une
// courbe lisible, pas trop pour rester rapide (7 jours en brut = 60 000 points).
const MOYENNE = { "-6h": "5m", "-24h": "15m", "-7d": "1h" };

// Nom affiché et unité de chaque champ de l'API
const CHAMPS = {
  pm1: ["PM1", "µg/m³"], pm25: ["PM2.5", "µg/m³"], pm10: ["PM10", "µg/m³"],
  temp: ["Température", "°C"], hum: ["Humidité", "%"], co2: ["CO₂", "ppm"],
};

const graphes = {};   // les graphes déjà dessinés, à remplacer au rechargement

// Demande les mesures au serveur et renvoie la liste des points.
async function chargerMesures(periode) {
  const params = new URLSearchParams({ key: CLE, start: periode, freq: MOYENNE[periode] });
  if (DEVICE_ID) params.set("device_id", DEVICE_ID);
  const reponse = await fetch("https://api.aircarto.fr/iut-amu/data?" + params);
  const json = await reponse.json();
  if (!json.ok) {
    // Le serveur explique toujours l'erreur dans "message"
    throw new Error(json.message);
  }
  return json.points;
}

// Dessine (ou redessine) un graphe avec une courbe par champ.
function dessiner(idCanvas, points, champs) {
  const etiquettes = points.map(p => new Date(p.time).toLocaleString("fr-FR",
    { weekday: "short", hour: "2-digit", minute: "2-digit" }));
  const courbes = champs
    .filter(c => points.some(p => p[c] !== undefined))   // seulement les champs mesurés
    .map(c => ({
      label: CHAMPS[c][0],
      data: points.map(p => p[c] ?? null),               // null = trou dans la courbe
      pointRadius: 0,
      borderWidth: 2,
    }));

  if (graphes[idCanvas]) graphes[idCanvas].destroy();
  graphes[idCanvas] = new Chart(document.getElementById(idCanvas), {
    type: "line",
    data: { labels: etiquettes, datasets: courbes },
    options: { interaction: { mode: "index", intersect: false } },
  });
  return courbes.length;
}

// Affiche la valeur la plus récente de chaque champ.
function afficherDerniere(points) {
  const derniere = points[points.length - 1];
  const html = Object.keys(CHAMPS)
    .filter(c => derniere[c] !== undefined)
    .map(c => {
      const [nom, unite] = CHAMPS[c];
      const valeur = derniere[c].toFixed(c === "co2" ? 0 : 1);   // CO2 sans décimale
      return `<div class="valeur">${nom}<b>${valeur} ${unite}</b></div>`;
    });
  document.getElementById("valeurs").innerHTML = html.join("");
}

async function rafraichir() {
  const etat = document.getElementById("etat");
  const periode = document.getElementById("periode").value;
  try {
    const points = await chargerMesures(periode);
    if (points.length === 0) {
      etat.textContent = "Aucune mesure sur cette période : la carte envoie-t-elle ?";
      return;
    }
    afficherDerniere(points);
    dessiner("graphe-pm", points, ["pm1", "pm25", "pm10"]);
    dessiner("graphe-meteo", points, ["temp", "hum"]);
    const avecCo2 = dessiner("graphe-co2", points, ["co2"]);
    // Pas de CO2 sur un NebuleAir : on cache son graphe
    document.getElementById("carte-co2").hidden = (avecCo2 === 0);

    const date = new Date(points[points.length - 1].time).toLocaleString("fr-FR");
    etat.className = "";
    etat.textContent = `${points.length} points, moyenne sur ${MOYENNE[periode]}. `
                     + `Dernière mesure : ${date}.`;
  } catch (erreur) {
    etat.className = "erreur";
    etat.textContent = "Erreur : " + erreur.message;
  }
}

document.getElementById("periode").addEventListener("change", rafraichir);
rafraichir();
setInterval(rafraichir, 5 * 60 * 1000);   // mise à jour toutes les 5 minutes
</script>
</body>
</html>