/****************** CONFIG UTILISATEUR ******************/
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <WebServer.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <time.h>
// >>>>>>>>>>>> RENSEIGNE ICI <<<<<<<<<<<<
const char* WIFI_SSID = "SSID";
const char* WIFI_PASS = "PASS";
// ============== IP STATIQUE (optionnelle) ==============
// Passe à true pour forcer une IP fixe. Sinon DHCP.
const bool USE_STATIC_IP = true;
// Choisis une IP libre, en dehors de la plage DHCP de ton routeur.
IPAddress LOCAL_IP (192,168,1,200);
IPAddress GATEWAY (192,168,1,1);
IPAddress SUBNET (255,255,255,0);
IPAddress PRIMARY_DNS(8,8,8,8);
IPAddress SECOND_DNS (8,8,4,4);
// URL de l'application web Apps Script (se termine par /exec)
const char* SCRIPT_URL = "https://script.googleusercontent.com/macros/echo?user_content_key=AehSKLjoiIKV-yyiBaXcaGP2f910Sqky1We4kKk5rKe_DBb3e-2gJYuVhNso6JDjlzQQWlmHPxvU9eooQKcGtboqwq1bGP0MEGg350UhS8JFn_0S2kvYwfMhEcNJ6hGLXzcCASQWr3Dia2q1X6R9L6Itx8WwB8xcbzsVdja3KY-Qsq3nMVwsTluyOM378AbWgM3DuE7J85otH2cvw8xXI8Mre33_PdwZ6Lwu2FwTvOKnM5HmbDswo2eVB4Hyq-5IOGPBfMpULgGTcMTwuQX1Sxbp-igi0kZUCw&lib=MbKdxQKzYhFOugb97Ifc6ICDIHcIHH2zA";
// Fréquences (en minutes)
uint32_t READ_EVERY_MIN = 60; // lecture des sondes toutes les X minutes
uint32_t LOG_EVERY_MIN = 360; // envoi automate vers Google toutes les X minutes
// Matériel
#define ONEWIRE_PIN 4 // Bus OneWire DS18B20 (avec pull-up 4.7k)
#define LED_BLUE_PIN 2 // LED bleue = Wi-Fi OK
#define BTN_NEXT_PIN 12 // bouton: sonde suivante (OLED)
#define BTN_SEND_PIN 14 // bouton: envoi immédiat (toutes sondes)
// OLED I2C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// NTP Europe/Brussels (DST auto)
const char* NTP_TZ = "CET-1CEST,M3.5.0/2,M10.5.0/3";
const char* NTP1 = "pool.ntp.org";
// ========== SONDES ==========
// 1) Inclure l’en-tête auto-généré (conseillé) :
#include "sondes_autogen.h" // contient: SONDE_COUNT, SONDE_NAMES[], SONDE_ADDR[]
// 2) OU coller ton tableau à la place et définir SONDE_COUNT, SONDE_NAMES, SONDE_ADDR.
/******** OneWire / Dallas ********/
OneWire oneWire(ONEWIRE_PIN);
DallasTemperature sensors(&oneWire);
/******** WebServer ********/
WebServer server(80);
/******** État courant ********/
struct SensorData {
bool present = false;
float tempC = NAN;
String lastTs = ""; // ISO local du dernier read
};
SensorData gData[SONDE_COUNT];
int gSelected = 0; // sonde affichée
bool gWifiOK = false;
bool gTimeSynced = false; // NTP OK ?
String gLastSendStatus = "-"; // "OK hh:mm:ss" | "ERR hh:mm:ss" | "-"
unsigned long tNextRead = 0;
unsigned long tNextLog = 0;
unsigned long tNextNtpResync = 0; // resync périodique
/******** Anti-rebond boutons ********/
bool readButton(uint8_t pin) {
static uint32_t lastChange[64] = {0};
static bool lastState[64] = {true}; // INPUT_PULLUP -> repos HIGH
bool s = digitalRead(pin);
if (s != lastState[pin]) {
if (millis() - lastChange[pin] > 30) { // 30ms
lastChange[pin] = millis();
lastState[pin] = s;
if (s == LOW) return true; // front descendant = appui
}
}
return false;
}
/******** Utils ********/
String addrToString(const DeviceAddress& a) {
char buf[24];
snprintf(buf, sizeof(buf), "%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X",
a[0],a[1],a[2],a[3],a[4],a[5],a[6],a[7]);
return String(buf);
}
bool isTimeSynced() {
time_t now = time(nullptr);
return (now > 1600000000); // ~2020-09-13
}
String nowISO() {
time_t now = time(nullptr);
struct tm t; localtime_r(&now, &t);
char buf[32];
strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &t);
return String(buf);
}
String nowPrettyHHMMSS() {
time_t now = time(nullptr);
struct tm t; localtime_r(&now, &t);
char buf[16];
strftime(buf, sizeof(buf), "%H:%M:%S", &t);
return String(buf);
}
String nowPrettyDate() {
time_t now = time(nullptr);
struct tm t; localtime_r(&now, &t);
char buf[16];
strftime(buf, sizeof(buf), "%d/%m/%Y", &t);
return String(buf);
}
void drawOLED() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// Ligne 1: Sonde + WiFi + NTP
display.setCursor(0,0);
display.print(SONDE_NAMES[gSelected]);
display.setCursor(72,0);
display.print(gWifiOK ? "WiFi:OK" : "WiFi:--");
display.setCursor(0,15);
display.print("NTP:");
display.print(gTimeSynced ? "OK" : "--");
// Ligne 2: adresse
//display.setCursor(0,22);
//display.print(addrToString(SONDE_ADDR[gSelected]));
// Ligne 3: Temp
display.setCursor(0,30);
if (gData[gSelected].present && !isnan(gData[gSelected].tempC)) {
display.printf("Temp: %.2f C", gData[gSelected].tempC);
} else {
display.print("Temp: --");
}
// Ligne 4: Heure/Date (si pas sync -> tirets)
display.setCursor(0,46);
if (gTimeSynced) {
display.print(nowPrettyDate());
display.setCursor(80,46);
display.print(nowPrettyHHMMSS());
} else {
display.print("Date: --/--/----");
display.setCursor(80,46);
display.print("--:--:--");
}
// Ligne 5/6: statut envoi
display.setCursor(0,56);
display.print("Send: ");
display.print(gLastSendStatus);
display.display();
}
/******** Wi-Fi ********/
void wifiConnect() {
WiFi.mode(WIFI_STA);
if (USE_STATIC_IP) {
if (!WiFi.config(LOCAL_IP, GATEWAY, SUBNET, PRIMARY_DNS, SECOND_DNS)) {
Serial.println("WiFi.config() a échoué — bascule DHCP");
}
}
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.printf("WiFi connecting to %s", WIFI_SSID);
uint32_t t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) {
delay(250); Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nWiFi OK: %s\n", WiFi.localIP().toString().c_str());
gWifiOK = true;
digitalWrite(LED_BLUE_PIN, HIGH);
} else {
// Si IP statique activée et échec, on tente un dernier fallback DHCP
if (USE_STATIC_IP) {
Serial.println("\nWiFi FAIL (static). Tentative DHCP de secours...");
WiFi.disconnect(true, true);
delay(200);
//WiFi.config(0U,0U,0U); // DHCP
WiFi.begin(WIFI_SSID, WIFI_PASS);
uint32_t t1 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t1 < 15000) {
delay(250); Serial.print(".");
}
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nWiFi OK (fallback DHCP): %s\n", WiFi.localIP().toString().c_str());
gWifiOK = true;
digitalWrite(LED_BLUE_PIN, HIGH);
} else {
Serial.println("\nWiFi FAIL");
gWifiOK = false;
digitalWrite(LED_BLUE_PIN, LOW);
}
}
}
/******** NTP ********/
void syncTimeBlocking() {
gTimeSynced = false;
if (!gWifiOK) return;
configTzTime(NTP_TZ, NTP1);
for (int i=0; i<20; i++) { // ~4s max
if (isTimeSynced()) { gTimeSynced = true; break; }
delay(200);
}
Serial.println(gTimeSynced ? "NTP synced" : "NTP not synced");
}
/******** Lectures ********/
void readAllSensors() {
sensors.requestTemperatures(); // conversion synchrone
for (size_t i=0; i<SONDE_COUNT; i++) {
float t = sensors.getTempC(SONDE_ADDR[i]);
bool ok = (t != DEVICE_DISCONNECTED_C && t > -100.0 && t < 125.0);
gData[i].present = ok;
gData[i].tempC = ok ? t : NAN;
gData[i].lastTs = gTimeSynced ? nowISO() : ""; // si pas sync, on laisse vide
}
}
static String urlEncode_(const String& s) {
String out; out.reserve(s.length()*3);
for (size_t i=0; i<s.length(); ++i) {
unsigned char c = (unsigned char)s[i];
if (('a'<=c && c<='z') || ('A'<=c && c<='Z') || ('0'<=c && c<='9') ||
c=='-' || c=='_' || c=='.' || c=='~') out += (char)c;
else if (c==' ') out += '+';
else { char b[4]; snprintf(b, sizeof(b), "%%%02X", c); out += b; }
}
return out;
}
bool postOne(const char* sondeName, float tempC, const char* mode) {
if (!gWifiOK || WiFi.status() != WL_CONNECTED) return false;
const String tsISO = gTimeSynced ? nowISO() : String();
// ---------- 1) JSON ----------
String body;
body.reserve(128);
body = "{\"sondeName\":\""; body += sondeName;
body += "\",\"tempC\":"; body += String(tempC, 2);
body += ",\"mode\":\""; body += mode; body += "\"";
if (tsISO.length()) { body += ",\"ts\":\""; body += tsISO; body += "\""; }
body += "}";
Serial.printf("[GAS] URL: '%s' (len=%d)\n", SCRIPT_URL, (int)strlen(SCRIPT_URL));
Serial.printf("[GAS] JSON len=%d\n", body.length());
Serial.println("[GAS] JSON body:");
Serial.println(body);
WiFiClientSecure client;
client.setInsecure(); // OK pour valider. Ensuite: setCACert(root) pour sécuriser.
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setConnectTimeout(10000);
http.setReuse(false);
if (!http.begin(client, SCRIPT_URL)) {
Serial.println("[GAS] http.begin() a échoué");
return false;
}
http.addHeader("Content-Type", "application/json");
http.addHeader("Accept", "application/json, text/plain, */*");
http.addHeader("User-Agent", "ESP32-Module/1.0");
int code = http.POST(body);
String err = http.errorToString(code);
String resp = (code > 0) ? http.getString() : "";
http.end();
Serial.printf("[GAS] JSON POST -> code=%d (%s)\n", code, err.c_str());
if (resp.length()) { Serial.println("[GAS] resp:"); Serial.println(resp); }
if (code >= 200 && code < 300) {
Serial.printf("POST %s -> %d OK\n", sondeName, code);
return true;
}
// ---------- 2) Fallback: application/x-www-form-urlencoded ----------
if (code == -1 || code == 400) {
String form;
form.reserve(128);
form = "sondeName="; form += urlEncode_(String(sondeName));
form += "&tempC="; form += String(tempC, 2);
form += "&mode="; form += urlEncode_(String(mode));
if (tsISO.length()) { form += "&ts="; form += urlEncode_(tsISO); }
Serial.printf("[GAS] Fallback FORM len=%d\n", form.length());
Serial.println("[GAS] FORM body:"); Serial.println(form);
if (!http.begin(client, SCRIPT_URL)) {
Serial.println("[GAS] http.begin() (fallback) a échoué");
return false;
}
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
http.addHeader("User-Agent", "ESP32-Module/1.0");
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setConnectTimeout(10000);
http.setReuse(false);
int code2 = http.POST(form);
String err2 = http.errorToString(code2);
String resp2 = (code2 > 0) ? http.getString() : "";
http.end();
Serial.printf("[GAS] FORM POST -> code=%d (%s)\n", code2, err2.c_str());
if (resp2.length()) { Serial.println("[GAS] resp (form):"); Serial.println(resp2); }
bool ok = (code2 >= 200 && code2 < 300);
Serial.printf("POST %s -> %d %s\n", sondeName, code2, ok ? "OK" : "FAIL");
return ok;
}
Serial.printf("POST %s -> %d FAIL\n", sondeName, code);
return false;
}
void sendAll(const char* mode) {
bool allOk = true;
for (size_t i=0; i<SONDE_COUNT; i++) {
if (!gData[i].present || isnan(gData[i].tempC)) continue;
bool ok = postOne(SONDE_NAMES[i], gData[i].tempC, mode);
if (!ok) allOk = false;
delay(30);
}
String hhmm = gTimeSynced ? nowPrettyHHMMSS() : "--:--:--";
gLastSendStatus = (allOk ? "OK " : "ERR ") + hhmm;
}
/******** Serveur web ********/
//WebServer server(80);
String htmlIndex() {
String ip = (gWifiOK ? WiFi.localIP().toString() : String("0.0.0.0"));
String now = (gTimeSynced ? nowISO() : String("--"));
String h = F("<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>Sondes DS18B20</title></head><body style='font-family:system-ui'>");
h += "<h1>ESP32 DS18B20</h1>";
h += "<p>IP: " + ip + " — Time: " + now + " — NTP: " + (gTimeSynced?"OK":"--") + "</p>";
h += "<p>Last send: " + gLastSendStatus + "</p>";
h += "<p><a href='/send-now'><button style='font-size:1.1rem;padding:.5rem .8rem'>Envoyer maintenant</button></a></p>";
h += "<p><a href='/sensors.json'>/sensors.json</a></p>";
h += "</body></html>";
return h;
}
void handleIndex() { server.send(200, "text/html; charset=utf-8", htmlIndex()); }
void handleSensorsJson() {
StaticJsonDocument<4096> doc;
doc["time"] = (gTimeSynced ? nowISO() : "");
doc["ntp"] = gTimeSynced;
JsonArray arr = doc.createNestedArray("sensors");
for (size_t i=0;i<SONDE_COUNT;i++) {
JsonObject o = arr.createNestedObject();
o["name"] = SONDE_NAMES[i];
o["addr"] = addrToString(SONDE_ADDR[i]);
o["present"]= gData[i].present;
if (gData[i].present && !isnan(gData[i].tempC)) {
o["tempC"] = gData[i].tempC;
o["ts"] = gData[i].lastTs; // vide si pas NTP
} else {
o["tempC"] = nullptr;
o["ts"] = nullptr;
}
}
String out; serializeJson(doc, out);
server.send(200, "application/json", out);
}
void handleSendNow() {
readAllSensors();
sendAll("bouton");
server.sendHeader("Location", "/");
server.send(302, "text/plain", "OK");
}
/******** Setup & Loop ********/
void setup() {
Serial.begin(115200);
pinMode(LED_BLUE_PIN, OUTPUT);
digitalWrite(LED_BLUE_PIN, LOW);
pinMode(BTN_NEXT_PIN, INPUT_PULLUP);
pinMode(BTN_SEND_PIN, INPUT_PULLUP);
// OLED
Wire.begin(); // SDA=21 SCL=22 (par défaut ESP32)
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("SSD1306 introuvable");
} else {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.println("Boot...");
display.display();
}
// Dallas
sensors.begin();
sensors.setResolution(12);
// Wi-Fi + NTP
//wifiConnect();
WiFi.mode(WIFI_STA);
// SSID Wokwi, pas de mot de passe. Le "6" (canal) rend la connexion plus rapide.
WiFi.begin("Wokwi-GUEST", "", 6);
Serial.print("Connexion WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(200);
Serial.print(".");
gWifiOK = true;
digitalWrite(LED_BLUE_PIN, HIGH);
}
Serial.println("\nConnecté !");
Serial.print("IP locale: ");
Serial.println(WiFi.localIP());
if (gWifiOK) syncTimeBlocking();
tNextNtpResync = millis() + 6UL*60UL*60UL*1000UL; // resync toutes les 6h
// Web
server.on("/", handleIndex);
server.on("/sensors.json", handleSensorsJson);
server.on("/send-now", handleSendNow);
server.begin();
// Première lecture
readAllSensors();
// Planif
tNextRead = millis() + READ_EVERY_MIN * 60UL * 1000UL;
tNextLog = millis() + LOG_EVERY_MIN * 60UL * 1000UL;
// Choisir la première sonde disponible
int first = 0;
for (size_t i=0;i<SONDE_COUNT;i++){ if (gData[i].present){ first = i; break; } }
gSelected = first;
drawOLED();
}
void loop() {
server.handleClient();
// Bouton Next (sonde suivante)
if (readButton(BTN_NEXT_PIN)) {
int count = gSelected;
do {
gSelected = (gSelected + 1) % SONDE_COUNT;
count++;
if (gData[gSelected].present) break;
} while (count < SONDE_COUNT);
drawOLED();
}
// Bouton Send (envoi immédiat)
if (readButton(BTN_SEND_PIN)) {
readAllSensors();
sendAll("bouton");
drawOLED();
}
// Watchdog Wi-Fi
static uint32_t tWifiCheck = 0;
if (millis() - tWifiCheck > 5000) {
tWifiCheck = millis();
if (WiFi.status() != WL_CONNECTED) {
gWifiOK = false; digitalWrite(LED_BLUE_PIN, LOW);
wifiConnect();
if (gWifiOK) syncTimeBlocking();
} else {
gWifiOK = true; digitalWrite(LED_BLUE_PIN, HIGH);
}
}
// Re-synchro NTP périodique
if ((long)(millis() - tNextNtpResync) >= 0) {
if (gWifiOK) syncTimeBlocking();
tNextNtpResync = millis() + 6UL*60UL*60UL*1000UL;
}
// Lecture périodique
if ((long)(millis() - tNextRead) >= 0) {
readAllSensors();
tNextRead = millis() + READ_EVERY_MIN * 60UL * 1000UL;
drawOLED();
}
// Envoi périodique (auto)
if ((long)(millis() - tNextLog) >= 0) {
sendAll("auto");
tNextLog = millis() + LOG_EVERY_MIN * 60UL * 1000UL;
drawOLED();
}
}