Projet balance connectée WiFi : HX711 + ESP32
- Datasheet HX711 complété et corrigé (brochage, timing, plages de gain, broche RATE) - Comparatif VL53L1X vs VL6180X - Fiche projet balance WiFi : câblage, alimentation, calibration web, dépannage - Code ESP32 : AP WiFi, portail captif, SSE temps réel, calibration depuis la page web (NVS) - Annexe installation Arduino IDE Windows 10 (pilote CP2102, support ESP32, bibliothèques) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
Balance connectée WiFi — ESP32 + HX711
|
||||
----------------------------------------
|
||||
- L'ESP32 crée un point d'accès WiFi (mode AP)
|
||||
- Un portail captif redirige automatiquement le smartphone vers la page de pesée
|
||||
- La page se met à jour en temps réel via Server-Sent Events (SSE)
|
||||
- La calibration se fait depuis la page web, le facteur est sauvegardé en NVS
|
||||
|
||||
Bibliothèques requises :
|
||||
- HX711 (bogde)
|
||||
- ESPAsyncWebServer (me-no-dev)
|
||||
- AsyncTCP (me-no-dev)
|
||||
- Preferences (intégrée ESP32)
|
||||
|
||||
Câblage HX711 :
|
||||
DOUT → GPIO 4
|
||||
SCK → GPIO 5
|
||||
VCC → 3V3 (ou 5V si module compatible)
|
||||
GND → GND
|
||||
*/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <DNSServer.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <Preferences.h>
|
||||
#include "HX711.h"
|
||||
|
||||
// --- Pins HX711 ---
|
||||
static const int PIN_DOUT = 4;
|
||||
static const int PIN_SCK = 5;
|
||||
|
||||
// --- WiFi AP ---
|
||||
static const char* AP_SSID = "Balance-ESP32";
|
||||
static const char* AP_PASS = ""; // réseau ouvert
|
||||
|
||||
// --- Objets globaux ---
|
||||
HX711 scale;
|
||||
DNSServer dnsServer;
|
||||
AsyncWebServer server(80);
|
||||
AsyncEventSource events("/events");
|
||||
Preferences prefs;
|
||||
|
||||
float calibrationFactor = 1.0f; // chargé depuis NVS au démarrage
|
||||
|
||||
// --- Flags inter-tâches (handler HTTP → loop) ---
|
||||
// AsyncWebServer tourne dans une tâche séparée ; les opérations HX711
|
||||
// doivent rester dans loop() pour éviter les conflits de timing.
|
||||
volatile bool pendingTare = false;
|
||||
volatile bool pendingCalib = false;
|
||||
volatile float pendingCalibMass = 0.0f;
|
||||
|
||||
// --- Page HTML embarquée ---
|
||||
static const char HTML[] PROGMEM = R"rawliteral(
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Balance</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px 30px 32px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.09);
|
||||
max-width: 400px;
|
||||
margin: 0 auto 20px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 { color: #555; font-size: 1.1em; font-weight: 500; margin-bottom: 28px; }
|
||||
h2 { color: #666; font-size: 1em; font-weight: 600; margin-bottom: 18px; text-align: left; }
|
||||
.weight { font-size: 5em; font-weight: 700; color: #1a1a2e; line-height: 1; }
|
||||
.unit { font-size: 1.3em; color: #888; margin-top: 8px; }
|
||||
.dot {
|
||||
display: inline-block; width: 10px; height: 10px;
|
||||
border-radius: 50%; background: #ccc;
|
||||
margin-right: 6px; vertical-align: middle;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.dot.live { background: #27ae60; }
|
||||
.status { font-size: 0.82em; color: #aaa; margin-top: 24px; }
|
||||
|
||||
/* Calibration */
|
||||
.steps { text-align: left; }
|
||||
.step { margin-bottom: 20px; }
|
||||
.step p { font-size: 0.9em; color: #555; margin-bottom: 8px; }
|
||||
.row { display: flex; gap: 8px; align-items: center; }
|
||||
input[type=number] {
|
||||
flex: 1; padding: 10px 12px; border: 1px solid #ddd;
|
||||
border-radius: 10px; font-size: 1em; outline: none;
|
||||
}
|
||||
input[type=number]:focus { border-color: #3498db; }
|
||||
button {
|
||||
padding: 10px 18px; border: none; border-radius: 10px;
|
||||
font-size: 0.95em; font-weight: 600; cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
button:active { opacity: 0.7; }
|
||||
.btn-tare { background: #ecf0f1; color: #555; }
|
||||
.btn-calib { background: #3498db; color: white; }
|
||||
.fb {
|
||||
font-size: 0.82em; margin-top: 6px; min-height: 1.2em;
|
||||
color: #27ae60;
|
||||
}
|
||||
.fb.err { color: #e74c3c; }
|
||||
.factor { font-size: 0.8em; color: #bbb; margin-top: 10px; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Affichage poids -->
|
||||
<div class="card">
|
||||
<h1>Balance connectée</h1>
|
||||
<div class="weight" id="w">—</div>
|
||||
<div class="unit">grammes</div>
|
||||
<div class="status">
|
||||
<span class="dot" id="dot"></span>
|
||||
<span id="status">Connexion…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calibration -->
|
||||
<div class="card">
|
||||
<h2>Calibration</h2>
|
||||
<div class="steps">
|
||||
|
||||
<div class="step">
|
||||
<p>① Vider la balance, puis mettre à zéro :</p>
|
||||
<button class="btn-tare" onclick="doTare()">Tare (zéro)</button>
|
||||
<div class="fb" id="fb-tare"></div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<p>② Poser une masse connue et entrer sa valeur :</p>
|
||||
<div class="row">
|
||||
<input type="number" id="mass" placeholder="Masse en grammes" min="1" step="any">
|
||||
<button class="btn-calib" onclick="doCalib()">Calibrer</button>
|
||||
</div>
|
||||
<div class="fb" id="fb-calib"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="factor" id="factor"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const wEl = document.getElementById('w');
|
||||
const dotEl = document.getElementById('dot');
|
||||
const statEl = document.getElementById('status');
|
||||
const fbTare = document.getElementById('fb-tare');
|
||||
const fbCalib = document.getElementById('fb-calib');
|
||||
const factorEl = document.getElementById('factor');
|
||||
|
||||
// --- SSE : mise à jour temps réel ---
|
||||
const src = new EventSource('/events');
|
||||
|
||||
src.addEventListener('weight', e => {
|
||||
wEl.textContent = parseFloat(e.data).toFixed(1);
|
||||
dotEl.classList.add('live');
|
||||
statEl.textContent = 'Mis à jour ' + new Date().toLocaleTimeString();
|
||||
});
|
||||
|
||||
src.addEventListener('info', e => {
|
||||
const msg = e.data;
|
||||
if (msg === 'tare_ok') {
|
||||
fbTare.className = 'fb';
|
||||
fbTare.textContent = '✓ Tare effectuée';
|
||||
} else if (msg.startsWith('calib_ok:')) {
|
||||
const f = parseFloat(msg.split(':')[1]);
|
||||
fbCalib.className = 'fb';
|
||||
fbCalib.textContent = '✓ Calibration sauvegardée';
|
||||
factorEl.textContent = 'Facteur : ' + f.toFixed(2);
|
||||
}
|
||||
});
|
||||
|
||||
src.onopen = () => { dotEl.classList.add('live'); statEl.textContent = 'Connecté'; };
|
||||
src.onerror = () => { dotEl.classList.remove('live'); statEl.textContent = 'Reconnexion…'; };
|
||||
|
||||
// --- Actions calibration ---
|
||||
function doTare() {
|
||||
fbTare.className = 'fb';
|
||||
fbTare.textContent = 'En cours…';
|
||||
fetch('/tare', { method: 'POST' }).catch(() => {
|
||||
fbTare.className = 'fb err';
|
||||
fbTare.textContent = 'Erreur réseau';
|
||||
});
|
||||
}
|
||||
|
||||
function doCalib() {
|
||||
const mass = parseFloat(document.getElementById('mass').value);
|
||||
if (!mass || mass <= 0) {
|
||||
fbCalib.className = 'fb err';
|
||||
fbCalib.textContent = 'Entrer une masse valide';
|
||||
return;
|
||||
}
|
||||
fbCalib.className = 'fb';
|
||||
fbCalib.textContent = 'En cours…';
|
||||
const body = new URLSearchParams({ mass: mass.toString() });
|
||||
fetch('/calibrate', { method: 'POST', body }).catch(() => {
|
||||
fbCalib.className = 'fb err';
|
||||
fbCalib.textContent = 'Erreur réseau';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)rawliteral";
|
||||
|
||||
// --- Portail captif ---
|
||||
static void handleCaptiveRedirect(AsyncWebServerRequest* req) {
|
||||
req->redirect("http://192.168.4.1/");
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(200);
|
||||
|
||||
// --- Chargement facteur de calibration depuis NVS ---
|
||||
prefs.begin("balance", true); // lecture seule
|
||||
calibrationFactor = prefs.getFloat("calib", 1.0f);
|
||||
prefs.end();
|
||||
Serial.printf("Facteur de calibration : %.2f%s\n",
|
||||
calibrationFactor,
|
||||
calibrationFactor == 1.0f ? " (défaut — calibration requise)" : " (chargé)");
|
||||
|
||||
// --- HX711 ---
|
||||
scale.begin(PIN_DOUT, PIN_SCK);
|
||||
if (!scale.is_ready()) {
|
||||
Serial.println("HX711 non détecté — vérifier câblage.");
|
||||
}
|
||||
scale.set_scale();
|
||||
scale.tare(20);
|
||||
scale.set_scale(calibrationFactor);
|
||||
Serial.println("HX711 prêt.");
|
||||
|
||||
// --- WiFi AP ---
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(AP_SSID, AP_PASS);
|
||||
Serial.printf("AP démarré — SSID : %s — IP : %s\n", AP_SSID,
|
||||
WiFi.softAPIP().toString().c_str());
|
||||
|
||||
// --- DNS captif ---
|
||||
dnsServer.start(53, "*", WiFi.softAPIP());
|
||||
|
||||
// --- Routes HTTP ---
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
req->send_P(200, "text/html", HTML);
|
||||
});
|
||||
|
||||
// Tare
|
||||
server.on("/tare", HTTP_POST, [](AsyncWebServerRequest* req) {
|
||||
pendingTare = true;
|
||||
req->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Calibration
|
||||
server.on("/calibrate", HTTP_POST, [](AsyncWebServerRequest* req) {
|
||||
if (req->hasParam("mass", true)) {
|
||||
float m = req->getParam("mass", true)->value().toFloat();
|
||||
if (m > 0.0f) {
|
||||
pendingCalibMass = m;
|
||||
pendingCalib = true;
|
||||
req->send(200, "text/plain", "OK");
|
||||
} else {
|
||||
req->send(400, "text/plain", "Masse invalide");
|
||||
}
|
||||
} else {
|
||||
req->send(400, "text/plain", "Parametre mass manquant");
|
||||
}
|
||||
});
|
||||
|
||||
// Portail captif Android / iOS
|
||||
server.on("/generate_204", HTTP_GET, handleCaptiveRedirect);
|
||||
server.on("/connecttest.txt", HTTP_GET, handleCaptiveRedirect);
|
||||
server.on("/redirect", HTTP_GET, handleCaptiveRedirect);
|
||||
server.on("/hotspot-detect.html", HTTP_GET, handleCaptiveRedirect);
|
||||
server.on("/library/test/success.html", HTTP_GET, handleCaptiveRedirect);
|
||||
server.onNotFound(handleCaptiveRedirect);
|
||||
|
||||
// --- SSE ---
|
||||
events.onConnect([](AsyncEventSourceClient* client) {
|
||||
client->send("0.0", "weight", millis(), 500);
|
||||
});
|
||||
server.addHandler(&events);
|
||||
|
||||
server.begin();
|
||||
Serial.println("Serveur web démarré.");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
dnsServer.processNextRequest();
|
||||
|
||||
// --- Tare ---
|
||||
if (pendingTare) {
|
||||
pendingTare = false;
|
||||
scale.set_scale(); // remet le facteur à 1 avant la tare
|
||||
scale.tare(20);
|
||||
scale.set_scale(calibrationFactor);
|
||||
events.send("tare_ok", "info", millis());
|
||||
Serial.println("Tare effectuée.");
|
||||
}
|
||||
|
||||
// --- Calibration ---
|
||||
if (pendingCalib) {
|
||||
pendingCalib = false;
|
||||
scale.set_scale(1.0f); // facteur neutre pour lire la valeur brute
|
||||
float raw = scale.get_value(20);
|
||||
float factor = raw / pendingCalibMass;
|
||||
calibrationFactor = factor;
|
||||
scale.set_scale(calibrationFactor);
|
||||
|
||||
prefs.begin("balance", false); // écriture
|
||||
prefs.putFloat("calib", calibrationFactor);
|
||||
prefs.end();
|
||||
|
||||
Serial.printf("Calibration : brut=%.0f masse=%.1fg facteur=%.2f\n",
|
||||
raw, pendingCalibMass, calibrationFactor);
|
||||
|
||||
String msg = "calib_ok:" + String(calibrationFactor, 2);
|
||||
events.send(msg.c_str(), "info", millis());
|
||||
}
|
||||
|
||||
// --- Lecture et diffusion du poids ---
|
||||
if (scale.is_ready()) {
|
||||
float grams = scale.get_units(5);
|
||||
events.send(String(grams, 1).c_str(), "weight", millis());
|
||||
Serial.printf("Poids : %.1f g\n", grams);
|
||||
}
|
||||
|
||||
delay(500);
|
||||
}
|
||||
Reference in New Issue
Block a user