quiet bee.Le labo ESP32 ↗LE LABO AU LYCÉE

SÉQUENCE 05 / Terminale · Projet de synthèse

La ruche connectée.

Mesurer, traiter, communiquer et valider.

La situation de départ

Un apiculteur souhaite consulter une température sans ouvrir systématiquement la ruche. L’équipe réalise d’abord une maquette : DS18B20, UNO R4 WiFi et navigateur sur le même réseau.

Une sonde renseigne sur son emplacement, pas sur l’état de santé de la colonie. Cette maquette ne commande ni chauffage, ni ventilation. Sur une vraie ruche, choix et pose se font avec l’apiculteur, sans gêner les abeilles.

ACQUÉRIRDS18B20→TRAITERUNO R4 WiFi→COMMUNIQUERHTTP / JSON

01 / Définir des exigences vérifiables

Exigence proposéeCritère de vérification
Acquérir périodiquementUn cycle toutes les 5 s environ
Identifier une donnée indisponibleErreur plutôt qu’une fausse valeur
Rendre la mesure accessiblePage locale et point /mesures JSON
Identifier une donnée périméeInvalide au-delà de 15 s
Borner l’attente HTTPRequête limitée à 1024 octets et 1,5 s
Mesurer même sans réseauLecture série maintenue ; tentatives Wi-Fi espacées

Ce cahier des charges de classe est à adapter. « Consulter depuis n’importe où » serait une exigence supplémentaire : le code fournit une consultation locale.

Schéma de branchement : La ruche connectée
Cliquer pour agrandir · SVG · PNG. Connecteurs déployés pour la lisibilité : suivez les noms des broches de votre carte.

02 / Mettre en service

  1. Reprendre le câblage et les bibliothèques du TP température avec une UNO R4 WiFi.
  2. Ouvrir le projet 05_ruche_wifi. WiFiS3 est fourni par Arduino UNO R4 Boards ; ne pas utiliser la bibliothèque WiFi d’une autre carte.
  3. Dans arduino_secrets.h, renseigner le réseau autorisé et son mot de passe, dans votre copie seulement.
  4. Téléverser ; ouvrir le moniteur à 115200 bauds et relever l’adresse affichée après connexion.
  5. Sur le même réseau, ouvrir http://adresse-affichée/ puis http://adresse-affichée/mesures.

Un réseau scolaire ou invité peut isoler ses appareils ; vérifier ce point avec son responsable. Aucun port du routeur ne doit être ouvert sur Internet pour ce TP.

03 / Le programme complet

05_ruche_wifi.ino

Projet complet ↓
// UNO R4 WiFi seulement. Prototype de classe sur reseau local de confiance.
#include <WiFiS3.h>
#include "arduino_secrets.h"

#include <OneWire.h>
#include <DallasTemperature.h>
#include <math.h>

constexpr uint8_t BROCHE_DQ = 4;
constexpr unsigned long MESURE_MS = 5000;
constexpr unsigned long CONVERSION_MS = 800; // Marge au-dela des 750 ms a 12 bits.
OneWire bus(BROCHE_DQ);
DallasTemperature capteurs(&bus);
DeviceAddress adresse;
bool conversion = false;
bool adresseConnue = false;
bool mesureValide = false;
float temperatureC = 0.0f;
unsigned long debutConversion = 0;
unsigned long dernierCycle = 0;
unsigned long derniereMesure = 0;

void lancerMesure() {
  // Recherche du premier capteur : reconnexion possible apres debranchement.
  adresseConnue = capteurs.getAddress(adresse, 0);
  if (!adresseConnue) {
    mesureValide = false;
    Serial.println("Erreur : DS18B20 absent");
    return;
  }
  capteurs.setResolution(adresse, 12);
  capteurs.requestTemperatures();
  debutConversion = millis();
  conversion = true;
}

void gererMesure() {
  const unsigned long maintenant = millis();
  if (conversion && maintenant - debutConversion >= CONVERSION_MS) {
    conversion = false;
    const float t = capteurs.getTempC(adresse);
    mesureValide = isfinite(t) && t != DEVICE_DISCONNECTED_C && t >= -55 && t <= 125;
    if (mesureValide) {
      temperatureC = t;
      derniereMesure = millis();
      Serial.print("Temperature : "); Serial.print(t, 2); Serial.println(" deg C");
    } else {
      Serial.println("Erreur : lecture DS18B20 impossible");
    }
  }
  if (!conversion && maintenant - dernierCycle >= MESURE_MS) {
    dernierCycle = maintenant;
    lancerMesure();
  }
}

WiFiServer serveur(80);
WiFiClient client;
bool serveurPret = false;
bool tentativeFaite = false;
unsigned long dernierEssai = 0;
unsigned long dernierControleReseau = 0;
unsigned long debutClient = 0;
constexpr unsigned long REESSAI_MS = 30000;
constexpr unsigned long FRAICHEUR_MS = 15000;
char requete[96];
size_t tailleLigne = 0;
size_t totalRecu = 0;
bool premiereLigneLue = false;
bool ligneVide = true;

bool donneeFraiche() {
  return mesureValide && millis() - derniereMesure <= FRAICHEUR_MS;
}

void fermerClient() {
  client.stop();
  client = WiFiClient();
}

void repondre() {
  const bool accueil = strncmp(requete, "GET / HTTP/1.", 13) == 0;
  const bool json = strncmp(requete, "GET /mesures HTTP/1.", 20) == 0;
  client.println(accueil || json ? "HTTP/1.1 200 OK" : "HTTP/1.1 404 Not Found");
  client.println("Connection: close");
  client.println("Cache-Control: no-store");
  client.println(json ? "Content-Type: application/json" : "Content-Type: text/html; charset=utf-8");
  client.println();
  if (json) {
    client.print("{\"temperature_c\":");
    if (donneeFraiche()) client.print(temperatureC, 2); else client.print("null");
    client.print(",\"valide\":"); client.print(donneeFraiche() ? "true" : "false");
    client.print(",\"age_ms\":");
    if (mesureValide) client.print(millis() - derniereMesure); else client.print("null");
    client.println("}");
  } else if (accueil) {
    client.println("<!doctype html><html lang='fr'><meta charset='utf-8'><meta name='viewport' content='width=device-width'><meta http-equiv='refresh' content='10'><title>Ruche : mesure locale</title><body><h1>Ruche pedagogique</h1>");
    client.print("<p>Temperature : <strong>");
    if (donneeFraiche()) {client.print(temperatureC, 2); client.print(" &deg;C");}
    else client.print("mesure indisponible");
    client.println("</strong></p><p>Actualisation de la page : 10 s. Acquisition : 5 s.</p>");
    client.println("<p>Une seule sonde ; aucune conclusion sanitaire automatique.</p><a href='/mesures'>Donnees JSON</a></body></html>");
  } else client.println("<h1>404</h1><a href='/'>Accueil</a>");
  fermerClient();
}

void gererHTTP() {
  if (!serveurPret) return;
  if (!client) {
    client = serveur.available();
    if (!client) return;
    debutClient = millis();
    tailleLigne = 0; totalRecu = 0;
    premiereLigneLue = false; ligneVide = true;
    requete[0] = '\0';
  }
  if (!client.connected() || millis() - debutClient > 1500) {fermerClient(); return;}
  // Au plus 64 caracteres traites par passage ; taille et duree bornees.
  for (uint8_t n = 0; n < 64 && client.available(); ++n) {
    const char c = char(client.read());
    if (++totalRecu > 1024) {fermerClient(); return;}
    if (!premiereLigneLue && c != '\r' && c != '\n') {
      if (tailleLigne >= sizeof(requete) - 1) {fermerClient(); return;}
      requete[tailleLigne++] = c;
      requete[tailleLigne] = '\0';
    }
    if (c == '\n') {
      if (ligneVide) {repondre(); return;}
      premiereLigneLue = true;
      ligneVide = true;
    } else if (c != '\r') ligneVide = false;
  }
}

void gererReseau() {
  const unsigned long maintenant = millis();
  if (maintenant - dernierControleReseau < 1000) return;
  dernierControleReseau = maintenant;
  const int etat = WiFi.status();
  if (etat == WL_CONNECTED) {
    if (!serveurPret) {
      serveur.begin(); serveurPret = true;
      Serial.print("Page locale : http://"); Serial.println(WiFi.localIP());
    }
    return;
  }
  if (serveurPret) {serveurPret = false; fermerClient();}
  if (!tentativeFaite || maintenant - dernierEssai >= REESSAI_MS) {
    tentativeFaite = true; dernierEssai = maintenant;
    if (etat == WL_NO_MODULE) {Serial.println("Module WiFi indisponible"); return;}
    if (SECRET_SSID[0] == '\0') {Serial.println("Configurer arduino_secrets.h"); return;}
    Serial.println("Tentative WiFi (l'appel de bibliotheque peut attendre)");
    WiFi.begin(SECRET_SSID, SECRET_PASS);
  }
}

void setup() {
  Serial.begin(115200);
  capteurs.begin();
  capteurs.setWaitForConversion(false);
  dernierCycle = millis();
  lancerMesure();
}

void loop() {
  gererMesure();
  gererReseau();
  gererHTTP();
}

Second onglet : arduino_secrets.h

#pragma once
// Renseigner uniquement dans votre copie locale. Ne pas publier ce fichier rempli.
#define SECRET_SSID ""
#define SECRET_PASS ""

Renseignez le SSID et le mot de passe dans votre copie locale. Le ZIP contient cet onglet vide ; ne partagez jamais le projet avec vos vrais identifiants.

04 / Comprendre le fonctionnement

PartieRôle
gererMesure()Conversion et validité comme au TP précédent.
gererReseau()Contrôle la connexion, démarre le serveur, espace les tentatives à 30 s.
gererHTTP()Traite au plus 64 caractères par passage ; ferme les clients trop lents ou bavards.
donneeFraiche()Vérifie validité et âge.
/mesuresRenvoie température, validité, âge ; null si indisponible.

La page se recharge toutes les 10 s, la sonde est interrogée toutes les 5 s : deux périodes distinctes. Aucun historique n’est conservé après extinction.

Conversion et lecture HTTP sont organisées sans longue attente dans la boucle. Les appels WiFiS3, notamment WiFi.begin() et les écritures réseau, restent synchrones et peuvent suspendre les autres tâches. Ce prototype n’est pas un système temps réel strict.

Le serveur HTTP, sans chiffrement ni authentification, est prévu pour un réseau local de confiance. Une exposition publique demande une architecture sécurisée distincte.

05 / Valider avant le rucher

EssaiRésultat attenduPreuve
Sonde stabiliséeSérie et page cohérentesCapture et comparaison à une référence
Capteur absent au démarrageErreur série ; null sur la page JSONJournal et capture
Coupure du Wi-FiMesures série poursuivies, tentatives espacéesRelevé chronologique
Retour du réseauAdresse affichée et page accessibleCapture après reconnexion
Client lent ou requête trop longueConnexion fermée ; boucle disponibleProtocole et durée
Extinction / redémarragePas de réutilisation silencieuse d’une ancienne mesureEssai de démarrage

Les programmes sont compilés pour les cartes indiquées ; ces essais physiques restent à réaliser avec le matériel de la classe. Rapporter aussi les échecs et expliquer leurs causes.

06 / Du prototype au système de terrain

Première : représenter

Tracer les blocs, nommer les flux d’information et d’énergie, expliquer le trajet de la donnée jusqu’au navigateur.

Terminale : justifier

Comparer Wi-Fi, stockage local et réseau cellulaire selon portée, disponibilité, coût, consommation et maintenance.

Hors de portée Wi-Fi, ce sketch ne suffit pas. Le parcours ESP32 + SHT85 + LTE-M présente une autre architecture ; il ne s’agit pas de recopier sa radio sur le schéma UNO.

Humidité, poids et température extérieure sont des évolutions possibles. Choisir les capteurs, vérifier tensions et niveaux logiques, concevoir le montage et adapter le code. Ces mesures ne sont pas produites par cette maquette.

Étudier le boîtier, la condensation, le passage des câbles et les perturbations pour la colonie. Mesurer le courant moyen : P = U × I, puis autonomie ≈ énergie utile / puissance moyenne, en intégrant pertes et cycles radio. Aucune autonomie n’est annoncée sans mesure.

À VOUS DE JOUER / PREMIÈRE

Pourquoi une sonde dans un coin de ruche ne représente-t-elle pas toutes les températures ?

PLUS LOIN / TERMINALE

Proposer trois exigences pour un rucher isolé, des méthodes d’essai et deux architectures de communication.

Pistes de correction

Gradient spatial et inertie limitent la représentativité. Disponibilité de la liaison, autonomie et tenue à l’environnement demandent chacune un critère mesurable et un protocole.

Le livrable de l’équipe

Présenter besoin et limites, schéma annoté, programme commenté, tableau d’essais, estimation énergétique fondée sur des mesures et amélioration argumentée. La qualité de la validation compte autant que la démonstration.

Revenir au parcours →

SOURCES ET PROLONGEMENTS

Vérifier dans la documentation.

Contenus originaux Quiet Bee. Progression proposée pour travailler la chaîne d’information, le prototypage et la validation ; elle ne remplace pas le programme ni les consignes de votre professeur.