I want to create a PCB BOARD OF A SMART WATER METER aquameter , and this is the .ino sketch :/*
- ================================================================
- AquaMetre Village — Sketch ESP32 DevKit V1
- Micro-compteur d'eau autonome — alimentation LiPo + turbine
- SANS électrovanne — mesure pure débit + conductivité
- ================================================================
- VERSION : 5.4
- MODIFICATIONS v5.4 vs v5.3 :
- ─────────────────────────────────────────────────────────────
- [1] ECRAN : remplacement Adafruit_SSD1306 → SH1106 1.3"
-
Bibliothèque utilisée : U8g2lib (U8G2_SH1106_128X64_NONAME_F_HW_I2C)
-
→ Pilote natif SH1106, rendu pixel-perfect sur écran 1.3"
-
→ API U8g2 : u8g2.clearBuffer() / sendBuffer() / setFont() etc.
- [2] PAGE 0 — Affichage consommation repensé :
-
• GRAND FORMAT → Volume en Litres (police 28px, ex : 125.4 L)
-
• PETIT FORMAT → Index en m³ (police 10px, ex : 0.1254 m3)
-
→ Plus lisible d'un coup d'œil pour l'utilisateur final
- [CONSERVÉ] Tous les correctifs et la logique v5.3 :
-
- FIX bug comptage à vide (pulses == 0 → return immédiat)
-
- Seuils TDS H=180/L=100, lecture 100ms
-
- Confirmation 2 lectures EAU / 3 lectures AIR
-
- Sauvegarde EEPROM 5min / 10L
- BIBLIOTHÈQUES REQUISES (Gestionnaire de bibliothèques Arduino) :
- ─────────────────────────────────────────────────────────────
- • U8g2 par oliver (chercher "U8g2")
- • RTClib par Adafruit
- • ArduinoJson
- ARCHITECTURE MATÉRIELLE :
- ─────────────────────────────────────────────────────────────
- CHAÎNE ÉNERGIE :
- Turbine hydro 5V (DN15/DN20 inline)
- └→ TP4056 → LiPo 3.7V/2000–3000mAh → MT3608 boost → 5V ESP32
- CAPTEURS :
- ┌─ Débitmètre YF-S201 (1–30 L/min) → GPIO4
- ├─ Sonde conductivité inox 2 électrodes → GPIO34 (ADC)
- │ VCC sonde → GPIO5 (transistor NPN anti-électrolyse)
- ├─ OLED SH1106 128×64 1.3" → I2C GPIO21/22
- ├─ RTC DS3231 → I2C GPIO21/22
- └─ Pont diviseur batterie → GPIO35 (ADC)
- BROCHAGE ESP32 DevKit V1 :
- GPIO 4 → YF-S201 signal débitmètre
- GPIO 5 → Transistor NPN VCC sonde TDS
- GPIO 21 → SDA I2C
- GPIO 22 → SCL I2C
- GPIO 26 → LED VERTE (EAU détectée)
- GPIO 27 → LED ROUGE (AIR détecté / alerte)
- GPIO 34 → ADC sonde TDS
- GPIO 35 → ADC batterie LiPo
- GPIO 0 → Bouton reset période
- ================================================================
*/
// ───────────────────────────────────────────────
// BIBLIOTHÈQUES
// ───────────────────────────────────────────────
#include
#include
#include
#include // ← SH1106 (remplace Adafruit_SSD1306)
#include
#include
#include
// ───────────────────────────────────────────────
// !! MODIFIER CES VALEURS AVANT DE PROGRAMMER !!
// ───────────────────────────────────────────────
const char* WIFI_SSID = "NomDeVotreWiFi";
const char* WIFI_PASSWORD = "MotDePasseWiFi";
const char* AP_SSID = "AquaMetre-M002";
const char* AP_PASSWORD = "aqua1234";
IPAddress AP_IP (192, 168, 4, 1);
IPAddress AP_SUBNET (255, 255, 255, 0);
#define COMPTEUR_ID "M-002"
#define COMPTEUR_MAISON "Entrer le nom"
#define PRIX_PAR_M3 45.0
// ───────────────────────────────────────────────
// MODE CALIBRATION SONDE TDS
// ───────────────────────────────────────────────
#define CALIBRATION_MODE false
// ───────────────────────────────────────────────
// CONFIGURATION ÉCRAN SH1106 1.3" (I2C)
//
// Le SH1106 est différent du SSD1306 : contrôleur colonne décalé
// de 2 pixels. U8g2 gère cela nativement avec le bon driver.
//
// Adresse I2C par défaut : 0x3C (peut être 0x3D selon cavalier)
// Brochage : SDA=GPIO21 / SCL=GPIO22 (I2C matériel ESP32)
// ───────────────────────────────────────────────
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(
U8G2_R0, // rotation 0°
/* reset= / U8X8_PIN_NONE,
/ clock= / 22, // SCL
/ data= */ 21 // SDA
);
// ───────────────────────────────────────────────
// CONFIGURATION RTC DS3231
// ───────────────────────────────────────────────
RTC_DS3231 rtc;
// ───────────────────────────────────────────────
// CONFIGURATION EEPROM
// ───────────────────────────────────────────────
#define EEPROM_SIZE 512
#define ADDR_INITIALIZED 0
#define ADDR_TOTAL_M3 4
#define ADDR_PERIODE_M3 8
#define ADDR_AIR_COUNT 12
void eepromWriteFloat (int addr, float val) { EEPROM.put(addr, val); EEPROM.commit(); }
float eepromReadFloat (int addr) { float v; EEPROM.get(addr, v); return v; }
void eepromWriteUint32 (int addr, uint32_t val) { EEPROM.put(addr, val); EEPROM.commit(); }
uint32_t eepromReadUint32 (int addr) { uint32_t v; EEPROM.get(addr, v); return v; }
// ───────────────────────────────────────────────
// CONFIGURATION DÉBITMÈTRE YF-S201
// ───────────────────────────────────────────────
#define FLOW_PIN 4
#define FLOW_INTERVAL_MS 1000
volatile uint32_t pulseCount = 0;
float flowRate_Lmin = 0.0;
float totalVolume_L = 0.0;
float totalVolume_m3 = 0.0;
float periodeVolume_m3 = 0.0;
unsigned long lastFlowCalc = 0;
// ───────────────────────────────────────────────
// CONFIGURATION SONDE TDS INOX 2 ÉLECTRODES
// ───────────────────────────────────────────────
#define CONDUCT_PIN 34
#define CONDUCT_VCC_PIN 5
#define CONDUCT_THRESHOLD_HIGH 180
#define CONDUCT_THRESHOLD_LOW 100
#define CONDUCT_CONFIRM_WATER 2
#define CONDUCT_CONFIRM_AIR 3
bool isWaterDetected = false;
uint16_t conductValue = 0;
uint16_t conductRaw = 0;
uint32_t airFilterCount = 0;
uint8_t confirmWaterCount = 0;
uint8_t confirmAirCount = 0;
// ───────────────────────────────────────────────
// CONFIGURATION BATTERIE LiPo
// ───────────────────────────────────────────────
#define BAT_PIN 35
#define BAT_FULL_V 4.2f
#define BAT_EMPTY_V 3.0f
float batteryVoltage = 0.0;
uint8_t batteryPercent = 0;
// ───────────────────────────────────────────────
// BROCHES LED ET BOUTON
// ───────────────────────────────────────────────
#define LED_GREEN_PIN 26
#define LED_RED_PIN 27
#define BTN_PIN 0
// ───────────────────────────────────────────────
// SERVEUR WEB HTTP
// ───────────────────────────────────────────────
WebServer server(80);
bool wifiConnected = false;
// ───────────────────────────────────────────────
// VARIABLES AFFICHAGE OLED
// ───────────────────────────────────────────────
uint8_t displayPage = 0;
unsigned long lastDisplay = 0;
unsigned long lastPage = 0;
// ================================================================
// INTERRUPTION — DÉBITMÈTRE YF-S201
// ================================================================
void IRAM_ATTR flowISR() {
pulseCount++;
}
// ================================================================
// SONDE TDS — LECTURE OPTIMISÉE
// ================================================================
uint16_t readConductivity() {
digitalWrite(CONDUCT_VCC_PIN, HIGH);
delay(5);
uint16_t samples[8];
uint32_t sum = 0;
uint16_t vmin = 4095, vmax = 0;
for (int i = 0; i vmax) vmax = samples[i];
delay(2);
}
digitalWrite(CONDUCT_VCC_PIN, LOW);
sum -= vmin;
sum -= vmax;
conductRaw = (uint16_t)(sum / 6);
return conductRaw;
}
// ================================================================
// SONDE TDS — DÉTECTION EAU AVEC HYSTÉRÉSIS + CONFIRMATION
// ================================================================
bool checkWaterHysteresis(uint16_t adc) {
if (!isWaterDetected) {
if (adc >= CONDUCT_THRESHOLD_HIGH) {
confirmWaterCount++;
confirmAirCount = 0;
if (confirmWaterCount >= CONDUCT_CONFIRM_WATER) {
isWaterDetected = true;
confirmWaterCount = 0;
Serial.printf("[TDS] EAU CONFIRMEE : ADC=%u >= seuil H=%u (%u lectures)\n",
adc, CONDUCT_THRESHOLD_HIGH, CONDUCT_CONFIRM_WATER);
}
} else {
confirmWaterCount = 0;
}
} else {
if (adc = CONDUCT_CONFIRM_AIR) {
isWaterDetected = false;
confirmAirCount = 0;
Serial.printf("[TDS] AIR CONFIRME : ADC=%u = CONDUCT_THRESHOLD_HIGH) etat = "EAU";
else if (val %s\n",
i + 1, val, tension, CONDUCT_THRESHOLD_HIGH, CONDUCT_THRESHOLD_LOW, etat);
char buf[32];
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, 10, "MODE CALIBRATION");
sprintf(buf, "ADC: %u", val);
u8g2.drawStr(0, 24, buf);
sprintf(buf, "V: %.3f", tension);
u8g2.drawStr(0, 36, buf);
u8g2.drawStr(0, 50, etat);
u8g2.sendBuffer();
delay(1000);
}
Serial.println("[CALIB] Fin. Regler CONDUCT_THRESHOLD_HIGH/LOW puis CALIBRATION_MODE=false\n");
}
// ================================================================
// GESTION CONDUCTIVITÉ — lecture toutes les 100ms
// ================================================================
void handleConductivity() {
static unsigned long lastCheck = 0;
unsigned long now = millis();
if (now - lastCheck 0 ET isWaterDetected → comptage
// ================================================================
void calculateFlow() {
unsigned long now = millis();
if (now - lastFlowCalc 0.0f && pulses > 0) {
flowRate_Lmin = ((float)pulses / elapsed) / 7.5f;
} else {
flowRate_Lmin = 0.0f;
}
// GARDE 1 : débitmètre doit avoir tourné (FIX v5.3)
if (pulses == 0) return;
// GARDE 2 : sonde TDS doit confirmer la présence d'eau
if (flowRate_Lmin > 0.01f) {
if (isWaterDetected) {
float vol_L = (flowRate_Lmin / 60.0f) * elapsed;
totalVolume_L += vol_L;
totalVolume_m3 = totalVolume_L / 1000.0f;
periodeVolume_m3 += vol_L / 1000.0f;
} else {
airFilterCount++;
Serial.printf("[FILTRE] Debit %.1f L/min REJETE - TDS=%u = 0.0f && t = 0.0f && p = 0.001f) {
periodeVolume_m3 = p;
Serial.printf("[EEPROM] Periode restauree : %.4f m3\n", periodeVolume_m3);
} else {
periodeVolume_m3 = 0.0f;
Serial.printf("[EEPROM] Periode residuelle ignoree (%.6f m3 9999 L : police réduite pour tenir
dtostrf(totalVolume_L, 7, 0, bufL); // ex: " 12345"
u8g2.setFont(u8g2_font_logisoso20_tf);
u8g2.drawStr(0, 40, bufL);
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(110, 40, "L");
}
// ── Séparateur ───────────────────────────────────────────────
u8g2.drawHLine(0, 46, 128);
// ── Petit affichage : m³ + débit ─────────────────────────────
u8g2.setFont(u8g2_font_6x10_tf);
char bufM3[16], bufQ[16];
dtostrf(totalVolume_m3, 7, 4, bufM3); // " 0.1254"
dtostrf(flowRate_Lmin, 4, 1, bufQ); // " 3.2"
char line1[32];
sprintf(line1, "%s m3 %s L/m", bufM3, bufQ);
u8g2.drawStr(0, 56, line1);
// ── Dernière ligne : mensuel + état ──────────────────────────
char bufP[12];
dtostrf(periodeVolume_m3, 6, 3, bufP);
char line2[32];
sprintf(line2, "Mois:%sm3 Bat:%u%%", bufP, batteryPercent);
u8g2.drawStr(0, 64, line2);
u8g2.sendBuffer();
}
// ================================================================
// PAGE 1 : SONDE TDS
// ================================================================
void oledPage1_Capteurs() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, 9, "--- SONDE TDS v5.4 ---");
u8g2.drawHLine(0, 11, 128);
char buf[32];
sprintf(buf, "ADC brut : %u", conductRaw);
u8g2.drawStr(0, 22, buf);
sprintf(buf, "H:%u L:%u W:%u/%u",
CONDUCT_THRESHOLD_HIGH, CONDUCT_THRESHOLD_LOW,
confirmWaterCount, CONDUCT_CONFIRM_WATER);
u8g2.drawStr(0, 32, buf);
// Barre de progression ADC
uint8_t barLen = (uint8_t)map(conductRaw, 0, 4095, 0, 120);
uint8_t seuilH_x = (uint8_t)map(CONDUCT_THRESHOLD_HIGH, 0, 4095, 0, 120);
uint8_t seuilL_x = (uint8_t)map(CONDUCT_THRESHOLD_LOW, 0, 4095, 0, 120);
u8g2.drawFrame(0, 35, 120, 6);
u8g2.drawBox(0, 35, barLen, 6);
// Effacer la zone des marqueurs puis redessiner
u8g2.setDrawColor(2); // XOR
u8g2.drawVLine(seuilH_x, 33, 10);
u8g2.drawVLine(seuilL_x, 35, 6);
u8g2.setDrawColor(1); // normal
u8g2.drawStr(0, 52, isWaterDetected ? "Etat : EAU DETECTEE" : "Etat : AIR / SEC");
sprintf(buf, "Rejects: %us", airFilterCount);
u8g2.drawStr(0, 63, buf);
u8g2.sendBuffer();
}
// ================================================================
// PAGE 2 : BATTERIE
// ================================================================
void oledPage2_Batterie() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, 9, "-- LIPO + TURBINE --");
u8g2.drawHLine(0, 11, 128);
// Tension en grand
char bufV[10];
dtostrf(batteryVoltage, 4, 2, bufV);
u8g2.setFont(u8g2_font_logisoso20_tf);
u8g2.drawStr(0, 38, bufV);
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(70, 38, "V");
char bufPct[8];
sprintf(bufPct, "%u%%", batteryPercent);
u8g2.drawStr(85, 38, bufPct);
// Barre batterie
uint8_t batBar = (uint8_t)map(batteryPercent, 0, 100, 0, 100);
u8g2.drawFrame(0, 42, 104, 8);
u8g2.drawBox(2, 44, batBar, 4);
u8g2.drawFrame(104, 44, 4, 4); // borne positive
if (batteryPercent >= 98)
u8g2.drawStr(0, 56, "Batterie: PLEINE");
else if (batteryPercent > 20)
u8g2.drawStr(0, 56, "Batterie: OK");
else
u8g2.drawStr(0, 56, "Batterie: FAIBLE !");
u8g2.drawStr(0, 64, "MT3608: 3.7V->5V | TP4056");
u8g2.sendBuffer();
}
// ================================================================
// PAGE 3 : RÉSEAU WiFi
// ================================================================
void oledPage3_Reseau() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, 9, "--- RESEAU WiFi ---");
u8g2.drawHLine(0, 11, 128);
char buf[32];
sprintf(buf, "ID: %s", COMPTEUR_ID);
u8g2.drawStr(0, 22, buf);
sprintf(buf, "AP: %s", AP_SSID);
u8g2.drawStr(0, 32, buf);
String apIP = WiFi.softAPIP().toString();
sprintf(buf, "IP AP: %s", apIP.c_str());
u8g2.drawStr(0, 42, buf);
if (wifiConnected) {
String staIP = WiFi.localIP().toString();
sprintf(buf, "STA: %s", staIP.c_str());
} else {
sprintf(buf, "STA: non connecte");
}
u8g2.drawStr(0, 52, buf);
sprintf(buf, "Facture: %d DA", (int)(periodeVolume_m3 * PRIX_PAR_M3));
u8g2.drawStr(0, 64, buf);
u8g2.sendBuffer();
}
// ================================================================
// ROTATION DES PAGES OLED (4s par page)
// ================================================================
void updateDisplay() {
unsigned long now = millis();
if (now - lastPage >= 4000) {
displayPage = (displayPage + 1) % 4;
lastPage = now;
}
if (now - lastDisplay >= 500) {
switch (displayPage) {
case 0: oledPage0_Consommation(); break;
case 1: oledPage1_Capteurs(); break;
case 2: oledPage2_Batterie(); break;
case 3: oledPage3_Reseau(); break;
}
lastDisplay = now;
}
}
// ================================================================
// SERVEUR HTTP — JSON COMPLET
// ================================================================
String buildJSON() {
DateTime now = rtc.now();
char ts[20];
sprintf(ts, "%04d-%02d-%02dT%02d:%02d:%02d",
now.year(), now.month(), now.day(),
now.hour(), now.minute(), now.second());
StaticJsonDocument doc;
doc["id"] = COMPTEUR_ID;
doc["maison"] = COMPTEUR_MAISON;
doc["timestamp"] = ts;
doc["index_total_m3"] = serialized(String(totalVolume_m3, 4));
doc["index_total_L"] = serialized(String(totalVolume_L, 1));
doc["periode_m3"] = serialized(String(periodeVolume_m3, 4));
doc["debit_lmin"] = serialized(String(flowRate_Lmin, 2));
doc["conductivite_raw"] = conductRaw;
doc["seuil_tds_haut"] = CONDUCT_THRESHOLD_HIGH;
doc["seuil_tds_bas"] = CONDUCT_THRESHOLD_LOW;
doc["confirm_water_needed"] = CONDUCT_CONFIRM_WATER;
doc["confirm_air_needed"] = CONDUCT_CONFIRM_AIR;
doc["confirm_water_current"] = confirmWaterCount;
doc["confirm_air_current"] = confirmAirCount;
doc["eau_detectee"] = isWaterDetected;
doc["air_rejects_secondes"] = airFilterCount;
doc["batterie_pct"] = batteryPercent;
doc["batterie_v"] = serialized(String(batteryVoltage, 2));
doc["wifi_rssi"] = wifiConnected ? WiFi.RSSI() : 0;
doc["facture_da"] = serialized(String(periodeVolume_m3 * PRIX_PAR_M3, 0));
doc["ip_sta"] = wifiConnected ? WiFi.localIP().toString() : "---";
doc["ip_ap"] = WiFi.softAPIP().toString();
doc["systeme"] = "LiPo+turbine hydro+MT3608";
doc["electrovanne"] = false;
doc["calib_mode"] = CALIBRATION_MODE;
doc["version"] = "5.4";
String json;
serializeJson(doc, json);
return json;
}
// ================================================================
// SERVEUR HTTP — PAGE WEB PRINCIPALE
// ================================================================
void handleRoot() {
DateTime now = rtc.now();
char dateStr[20];
sprintf(dateStr, "%02d/%02d/%04d %02d:%02d",
now.day(), now.month(), now.year(),
now.hour(), now.minute());
String html = "";
html += "";
html += "";
html += "";
html += "";
html += "";
html += "AquaMetre — " + String(COMPTEUR_ID) + " v5.4";
html += "" + String(COMPTEUR_MAISON) + " · " + String(dateStr);
html += " · Autonome : LiPo + Turbine hydro";
if (CALIBRATION_MODE) {
html += "";
html += "MODE CALIBRATION ACTIF";
html += "";
html += "ADC sonde : " + String(conductRaw) + "";
html += " — Seuil H : " + String(CONDUCT_THRESHOLD_HIGH) + "";
html += " — Seuil L : " + String(CONDUCT_THRESHOLD_LOW) + "";
html += "";
}
// État eau/air
String eauClass = isWaterDetected ? "ok" : "warn";
String eauTxt = isWaterDetected ? "EAU DETECTEE" : "AIR — DEBIT REJETE";
html += "";
html += "";
html += "";
html += "LED VERTE";
html += "|";
html += "";
html += "LED ROUGE";
html += "" + eauTxt + "";
html += "";
html += "Confirmation EAU (";
html += String(CONDUCT_CONFIRM_WATER) + " lectures requises) :";
html += "";
for (int i = 0; i L";
html += "↳ " + String(totalVolume_m3, 4) + " m³";
html += "";
// Grille 3 colonnes
html += "";
html += "";
html += "Debit actuel";
html += "" + String(flowRate_Lmin, 1) + "L/min";
html += "";
html += String(isWaterDetected ? "COMPTE" : "REJETE") + "";
html += "";
html += "";
html += "Sonde TDS inox";
html += "" + String(conductRaw) + "ADC";
html += "H:" + String(CONDUCT_THRESHOLD_HIGH);
html += " / L:" + String(CONDUCT_THRESHOLD_LOW) + " / 4095";
uint8_t tpct = (uint8_t)map(conductRaw, 0, 4095, 0, 100);
uint8_t spctH = (uint8_t)map(CONDUCT_THRESHOLD_HIGH, 0, 4095, 0, 100);
uint8_t spctL = (uint8_t)map(CONDUCT_THRESHOLD_LOW, 0, 4095, 0, 100);
html += "";
html += "";
html += "";
html += "▮ H=vert ▮ L=rouge";
html += "";
String batColor = batteryPercent > 50 ? "bar-fg-green" :
batteryPercent > 20 ? "bar-fg-amber" : "bar-fg";
html += "";
html += "Batterie LiPo";
html += "" + String(batteryPercent) + "%";
html += "" + String(batteryVoltage, 2) + " V — MT3608 → 5V";
html += "";
html += "";
html += ""; // fin grid3
html += "";
html += "";
html += "Ce mois (eau validee)";
html += "" + String(periodeVolume_m3, 3) + "m3";
html += "" + String((int)(periodeVolume_m3 * PRIX_PAR_M3)) + " DA";
html += "";
html += "";
html += "Securite anti-air (secondes rejetees)";
html += "" + String(airFilterCount) + "s";
html += "Secondes de debit sans eau confirmee";
html += "";
html += "";
html += "";
html += "Alimentation autonome";
html += "";
html += "Turbine hydro DN15/DN20 5V";
html += "TP4056 chargeur LiPo";
html += "LiPo 3.7V 2000-3000mAh";
html += "MT3608 boost 3.7->5V";
html += "";
html += "Systeme passif — se recharge pendant le passage de l eau";
html += "";
html += "";
html += "Connexion WiFi";
html += "
AP : ";
html += "
" + String(AP_SSID) + " → http://192.168.4.1";
html += "
STA : ";
if (wifiConnected) {
html += "
http://" + WiFi.localIP().toString() + "";
} else {
html += "
Non connecte";
}
html += "";
html += "";
html += "JSON brut";
html += "Reset periode";
html += "Status";
html += "";
html += "";
server.send(200, "text/html; charset=UTF-8", html);
}
void handleReleve() {
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, "application/json", buildJSON());
Serial.println("[HTTP] Releve envoye a : " + server.client().remoteIP().toString());
}
void handleReset() {
float anciennePeriode = periodeVolume_m3;
periodeVolume_m3 = 0.0f;
eepromWriteFloat(ADDR_PERIODE_M3, 0.0f);
StaticJsonDocument doc;
doc["status"] = "reset_ok";
doc["ancienne_periode"] = serialized(String(anciennePeriode, 4));
doc["nouvelle_periode"] = "0.0000";
String json;
serializeJson(doc, json);
server.send(200, "application/json", json);
Serial.println("[HTTP] Reset periode effectue.");
}
void handleStatus() {
StaticJsonDocument doc;
doc["id"] = COMPTEUR_ID;
doc["online"] = true;
doc["bat_pct"] = batteryPercent;
doc["bat_v"] = serialized(String(batteryVoltage, 2));
doc["rssi"] = wifiConnected ? WiFi.RSSI() : 0;
doc["ip_sta"] = wifiConnected ? WiFi.localIP().toString() : "---";
doc["ip_ap"] = WiFi.softAPIP().toString();
doc["eau_detectee"] = isWaterDetected;
doc["conductivite_raw"] = conductRaw;
doc["seuil_haut"] = CONDUCT_THRESHOLD_HIGH;
doc["seuil_bas"] = CONDUCT_THRESHOLD_LOW;
doc["confirm_water_needed"] = CONDUCT_CONFIRM_WATER;
doc["confirm_air_needed"] = CONDUCT_CONFIRM_AIR;
doc["confirm_water_current"] = confirmWaterCount;
doc["confirm_air_current"] = confirmAirCount;
doc["debit_lmin"] = serialized(String(flowRate_Lmin, 2));
doc["air_rejects_secondes"] = airFilterCount;
doc["electrovanne"] = false;
doc["calib_mode"] = CALIBRATION_MODE;
doc["version"] = "5.4";
String json;
serializeJson(doc, json);
server.send(200, "application/json", json);
}
void handleNotFound() {
server.send(404, "application/json",
"{"error":"route inconnue","routes":["/","/releve","/reset","/status"]}");
}
// ================================================================
// CONNEXION WIFI
// ================================================================
void connectWiFi() {
Serial.println("\n[WiFi] Demarrage mode AP+STA...");
WiFi.mode(WIFI_AP_STA);
WiFi.softAPConfig(AP_IP, AP_IP, AP_SUBNET);
bool apStarted = (strlen(AP_PASSWORD) >= 8)
? WiFi.softAP(AP_SSID, AP_PASSWORD)
: WiFi.softAP(AP_SSID);
if (apStarted) {
Serial.printf("[WiFi] AP demarre : SSID=%s | IP=%s\n",
AP_SSID, WiFi.softAPIP().toString().c_str());
} else {
Serial.println("[WiFi] ERREUR : impossible de demarrer l AP !");
}
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, 10, "AP actif !");
u8g2.drawStr(0, 22, AP_SSID);
u8g2.drawStr(0, 34, "Connexion routeur...");
u8g2.sendBuffer();
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
uint8_t tries = 0;
while (WiFi.status() != WL_CONNECTED && tries = 2000) {
periodeVolume_m3 = 0.0f;
eepromWriteFloat(ADDR_PERIODE_M3, 0.0f);
Serial.println("[BTN] Reset periode par bouton physique.");
for (int i = 0; i = 30000) {
readBattery(); lastBat = millis();
}
// 7. Sauvegarde EEPROM périodique (toutes les 5 min)
static unsigned long lastSave = 0;
if (millis() - lastSave >= 300000) {
saveToEEPROM(); lastSave = millis();
}
// 8. Sauvegarde EEPROM par volume (tous les 10 litres)
static float lastSavedLiters = 0.0f;
if (totalVolume_L - lastSavedLiters >= 10.0f) {
saveToEEPROM(); lastSavedLiters = totalVolume_L;
}
// 9. Reconnexion WiFi STA (toutes les 30s)
static unsigned long lastWifiCheck = 0;
if (millis() - lastWifiCheck >= 30000) {
if (WiFi.status() != WL_CONNECTED) {
wifiConnected = false;
WiFi.reconnect();
Serial.println("[WiFi] Tentative de reconnexion STA...");
} else {
wifiConnected = true;
}
lastWifiCheck = millis();
}
// 10. Log série (toutes les 5s)
static unsigned long lastLog = 0;
if (millis() - lastLog >= 5000) {
Serial.printf(
"[%s] Debit:%.1fL/min | TDS:%u(H:%u/L:%u) | %s | ConfW:%u/%u ConfA:%u/%u | %.1fL / %.4fm3 | Bat:%.2fV(%u%%) | AirRejects:%us\n",
COMPTEUR_ID, flowRate_Lmin,
conductRaw, CONDUCT_THRESHOLD_HIGH, CONDUCT_THRESHOLD_LOW,
isWaterDetected ? "EAU-OK" : "AIR-REJETE",
confirmWaterCount, CONDUCT_CONFIRM_WATER,
confirmAirCount, CONDUCT_CONFIRM_AIR,
totalVolume_L, totalVolume_m3,
batteryVoltage, batteryPercent,
airFilterCount
);
lastLog = millis();
}
delay(10);
}
/*
- ================================================================
- RÉSUMÉ DES MODIFICATIONS v5.4
- ================================================================
- [1] MIGRATION SSD1306 → SH1106 1.3"
- ─────────────────────────────────────────────────────────────
- Bibliothèque : U8g2lib (chercher "U8g2" dans le gestionnaire)
- Driver : U8G2_SH1106_128X64_NONAME_F_HW_I2C
- Différence clé : le SH1106 a un offset de colonne interne de
- 2 pixels — U8g2 gère cela nativement, contrairement à
- Adafruit_SSD1306 qui produirait un décalage visuel.
- API U8g2 (remplacements) :
- display.begin() → u8g2.begin()
- display.clearDisplay() → u8g2.clearBuffer()
- display.display() → u8g2.sendBuffer()
- display.setTextSize(1) → u8g2.setFont(u8g2_font_6x10_tf)
- display.setTextSize(2) → u8g2.setFont(u8g2_font_logisoso20_tf)
- display.setCursor(x,y) → u8g2.drawStr(x, y+baseline, str)
- display.print(val) → sprintf(buf,…) + u8g2.drawStr(…)
- display.drawFastHLine → u8g2.drawHLine
- display.fillRect → u8g2.drawBox
- display.drawRect → u8g2.drawFrame
- display.drawFastVLine → u8g2.drawVLine
- Note U8g2 : l'ordonnée Y est la BASELINE du texte, pas le
- coin supérieur. Décaler de +8 à +10px par rapport à SSD1306.
- [2] PAGE 0 — Affichage Litres grand / m³ petit
- ─────────────────────────────────────────────────────────────
- Police grand format : u8g2_font_logisoso28_tf (28px)
- → Chiffres nets, idéals pour lecture rapide
- → Si totalVolume_L ≥ 10000 → bascule sur logisoso20_tf (20px)
- Police petit format : u8g2_font_6x10_tf (10px)
- → Affiche l'index en m³ sous le grand chiffre
- → Exemple : " 125.4 L" (grand) + "0.1254 m3" (petit)
- [CONSERVÉ INTÉGRAL] Toute la logique v5.3 :
-
- FIX bug comptage à vide (pulses == 0 → return)
-
- Résidus EEPROM ignorés (< 0.001 m3)
-
- Seuils TDS H=180/L=100, lecture 100ms, stab 5ms
-
- Confirmation 2 lectures EAU / 3 lectures AIR
-
- LEDs GPIO26 (verte=eau) / GPIO27 (rouge=air)
-
- Sauvegarde EEPROM 5min / 10L
-
- Serveur HTTP : /, /releve, /reset, /status
-
- JSON enrichi : ajout "index_total_L" en v5.4
- ================================================================
*/