/*
* Portaombrelli intelligente
* ESP32 + PIR + OLED + LED + buzzer
*
* Collegamenti:
* PIR OUT: GPIO 2
* LED: GPIO 23
* Buzzer: GPIO 18
* OLED SDA: GPIO 21
* OLED SCL: GPIO 22
* OLED indirizzo: 0x3C
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ArduinoJson.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
// ---------- Configurazione ----------
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASSWORD = "";
constexpr uint8_t PIN_PIR = 2;
constexpr uint8_t PIN_LED = 23;
constexpr uint8_t PIN_BUZZER = 18;
constexpr uint8_t PIN_SDA = 21;
constexpr uint8_t PIN_SCL = 22;
// true: simula pioggia per provare l'allarme.
// false: utilizza il meteo di Milano.
constexpr bool TEST_PIOGGIA = false;
// In Wokwi non serve attesa.
// Con un PIR fisico impostare 30000 o 60000.
constexpr uint32_t AVVIO_PIR_MS = 0;
constexpr uint32_t INTERVALLO_METEO_MS =
5UL * 60UL * 1000UL;
constexpr uint32_t RITENTA_METEO_MS =
60UL * 1000UL;
constexpr uint32_t MAX_ETA_METEO_MS =
10UL * 60UL * 1000UL;
constexpr uint32_t DURATA_ALLARME_MS = 6000;
constexpr uint32_t INTERVALLO_BIP_MS = 250;
// Coordinate di Milano.
const char* METEO_URL =
"https://api.open-meteo.com/v1/forecast"
"?latitude=45.4642&longitude=9.1900"
"¤t=temperature_2m,rain,showers,weather_code"
"&timezone=Europe%2FRome&forecast_days=1";
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool displayPresente = false;
// ---------- Stato meteo ----------
struct Meteo {
bool valido = false;
bool piove = false;
float temperatura = 0;
float pioggiaMm = 0;
int codice = -1;
uint32_t aggiornatoMs = 0;
char ora[6] = "--:--";
};
// Dichiarazioni esplicite per il preprocessore Arduino.
Meteo leggiMeteo();
void pubblicaMeteo(const Meteo& dati);
bool meteoDisponibile(const Meteo& dati, uint32_t adesso);
bool scaricaMeteo(Meteo& nuovo);
void gestisciMovimento(
bool movimento,
const Meteo& dati,
uint32_t adesso
);
void aggiornaDisplay(
bool movimento,
const Meteo& dati,
uint32_t adesso
);
Meteo meteoCondiviso;
// Protegge i dati condivisi tra loop e task meteo.
portMUX_TYPE meteoMux = portMUX_INITIALIZER_UNLOCKED;
// ---------- Stato allarme ----------
bool movimentoGestito = false;
bool allarmeAttivo = false;
bool bipAcceso = false;
uint32_t inizioAllarmeMs = 0;
// ---------- Accesso ai dati meteo ----------
Meteo leggiMeteo() {
portENTER_CRITICAL(&meteoMux);
Meteo copia = meteoCondiviso;
portEXIT_CRITICAL(&meteoMux);
return copia;
}
void pubblicaMeteo(const Meteo& dati) {
portENTER_CRITICAL(&meteoMux);
meteoCondiviso = dati;
portEXIT_CRITICAL(&meteoMux);
}
// ---------- Interpretazione del meteo ----------
bool codiceIndicaPioggia(int codice) {
switch (codice) {
// Pioviggine.
case 51:
case 53:
case 55:
case 56:
case 57:
// Pioggia.
case 61:
case 63:
case 65:
case 66:
case 67:
// Rovesci.
case 80:
case 81:
case 82:
return true;
default:
return false;
}
}
bool meteoDisponibile(
const Meteo& dati,
uint32_t adesso
) {
return dati.valido &&
static_cast<uint32_t>(
adesso - dati.aggiornatoMs
) < MAX_ETA_METEO_MS;
}
// ---------- Richiesta HTTPS ----------
bool scaricaMeteo(Meteo& nuovo) {
WiFiClientSecure client;
// Configurazione per il prototipo Wokwi.
// Per un dispositivo distribuito usare setCACert()
// e sincronizzare l'orologio.
client.setInsecure();
client.setHandshakeTimeout(8);
HTTPClient http;
http.setConnectTimeout(5000);
http.setTimeout(8000);
if (!http.begin(client, METEO_URL)) {
Serial.println(
"Impossibile avviare la richiesta meteo."
);
return false;
}
int stato = http.GET();
if (stato != HTTP_CODE_OK) {
Serial.printf(
"Errore meteo: HTTP %d\n",
stato
);
http.end();
return false;
}
String risposta = http.getString();
http.end();
JsonDocument doc;
DeserializationError errore =
deserializeJson(doc, risposta);
if (errore) {
Serial.printf(
"JSON non valido: %s\n",
errore.c_str()
);
return false;
}
JsonObjectConst corrente =
doc["current"].as<JsonObjectConst>();
if (
!corrente["rain"].is<float>() ||
!corrente["showers"].is<float>() ||
!corrente["weather_code"].is<int>() ||
!corrente["temperature_2m"].is<float>() ||
!corrente["time"].is<const char*>()
) {
Serial.println(
"Risposta meteo incompleta."
);
return false;
}
float pioggia =
corrente["rain"].as<float>();
float rovesci =
corrente["showers"].as<float>();
if (pioggia < 0 || rovesci < 0) {
return false;
}
nuovo.pioggiaMm = pioggia + rovesci;
nuovo.codice =
corrente["weather_code"].as<int>();
nuovo.temperatura =
corrente["temperature_2m"].as<float>();
// Pioggia presente quando ci sono precipitazioni
// liquide oppure un codice di pioggia/pioviggine.
nuovo.piove =
nuovo.pioggiaMm > 0 ||
codiceIndicaPioggia(nuovo.codice);
const char* ora =
corrente["time"].as<const char*>();
if (strlen(ora) >= 16) {
memcpy(nuovo.ora, ora + 11, 5);
nuovo.ora[5] = '\0';
}
nuovo.aggiornatoMs = millis();
nuovo.valido = true;
Serial.printf(
"Milano %s: codice=%d, pioggia=%.2f mm, piove=%s\n",
nuovo.ora,
nuovo.codice,
nuovo.pioggiaMm,
nuovo.piove ? "SI" : "NO"
);
return true;
}
// ---------- Task meteo ----------
// La rete lavora separatamente:
// PIR e allarme continuano a funzionare
// durante le richieste HTTPS.
void taskMeteo(void*) {
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
bool richiestaEseguita = false;
uint32_t ultimaRichiestaMs = 0;
uint32_t ultimaRiconnessioneMs = millis();
uint32_t intervalloMs = INTERVALLO_METEO_MS;
for (;;) {
uint32_t adesso = millis();
if (WiFi.status() != WL_CONNECTED) {
pubblicaMeteo(Meteo{});
richiestaEseguita = false;
if (
static_cast<uint32_t>(
adesso - ultimaRiconnessioneMs
) >= 15000
) {
Serial.println(
"Connessione WiFi in corso..."
);
WiFi.disconnect();
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
ultimaRiconnessioneMs = adesso;
}
} else if (
!richiestaEseguita ||
static_cast<uint32_t>(
adesso - ultimaRichiestaMs
) >= intervalloMs
) {
Meteo nuovo;
bool ok = scaricaMeteo(nuovo);
// In caso di errore il meteo risulta
// non disponibile.
pubblicaMeteo(nuovo);
intervalloMs = ok
? INTERVALLO_METEO_MS
: RITENTA_METEO_MS;
ultimaRichiestaMs = millis();
richiestaEseguita = true;
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// ---------- Movimento ----------
void gestisciMovimento(
bool movimento,
const Meteo& dati,
uint32_t adesso
) {
if (!movimento) {
movimentoGestito = false;
return;
}
if (
adesso < AVVIO_PIR_MS ||
movimentoGestito
) {
return;
}
if (
!TEST_PIOGGIA &&
!meteoDisponibile(dati, adesso)
) {
return;
}
// Un solo avviso finché il PIR resta HIGH.
movimentoGestito = true;
Serial.println("Movimento rilevato.");
if (
(TEST_PIOGGIA || dati.piove) &&
!allarmeAttivo
) {
allarmeAttivo = true;
inizioAllarmeMs = adesso;
Serial.println(
"PIOGGIA: prendi l'ombrello!"
);
}
}
// ---------- LED e buzzer ----------
void aggiornaAllarme(uint32_t adesso) {
if (
allarmeAttivo &&
static_cast<uint32_t>(
adesso - inizioAllarmeMs
) >= DURATA_ALLARME_MS
) {
allarmeAttivo = false;
}
bool deveSuonare =
allarmeAttivo &&
(
(
static_cast<uint32_t>(
adesso - inizioAllarmeMs
) / INTERVALLO_BIP_MS
) % 2 == 0
);
if (deveSuonare != bipAcceso) {
bipAcceso = deveSuonare;
digitalWrite(
PIN_LED,
bipAcceso ? HIGH : LOW
);
if (bipAcceso) {
tone(PIN_BUZZER, 2200);
} else {
noTone(PIN_BUZZER);
}
}
}
// ---------- Display OLED ----------
void aggiornaDisplay(
bool movimento,
const Meteo& dati,
uint32_t adesso
) {
if (!displayPresente) {
return;
}
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println(
TEST_PIOGGIA
? "PORTAOMBRELLI - TEST"
: "PORTAOMBRELLI"
);
if (allarmeAttivo) {
display.setTextSize(2);
display.setCursor(0, 16);
display.println("PRENDI");
display.setCursor(0, 36);
display.println("L'OMBRELLO");
display.setTextSize(1);
display.setCursor(0, 56);
display.print(
TEST_PIOGGIA
? "PIOGGIA SIMULATA"
: "MILANO: PIOGGIA"
);
} else {
display.setCursor(0, 14);
if (TEST_PIOGGIA) {
display.println("PIOGGIA SIMULATA");
} else if (
!meteoDisponibile(dati, adesso)
) {
display.println("METEO NON DISP.");
display.setCursor(0, 26);
display.println(
WiFi.status() == WL_CONNECTED
? "Attendo dati meteo..."
: "Connessione WiFi..."
);
} else {
display.println(
dati.piove
? "MILANO: PIOGGIA"
: "MILANO: NO PIOGGIA"
);
display.setCursor(0, 26);
display.printf(
"Pioggia: %.2f mm",
dati.pioggiaMm
);
display.setCursor(0, 38);
display.printf(
"%.1f C Dati:%s",
dati.temperatura,
dati.ora
);
}
display.setCursor(0, 54);
if (adesso < AVVIO_PIR_MS) {
display.print("PIR: avvio...");
} else {
display.print(
movimento
? "PIR: movimento"
: "PIR: in attesa"
);
}
}
display.display();
}
// ---------- Setup ----------
void setup() {
Serial.begin(115200);
pinMode(PIN_PIR, INPUT);
pinMode(PIN_LED, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_LED, LOW);
digitalWrite(PIN_BUZZER, LOW);
Wire.begin(PIN_SDA, PIN_SCL);
displayPresente =
display.begin(
SSD1306_SWITCHCAPVCC,
0x3C
);
if (displayPresente) {
display.setTextColor(SSD1306_WHITE);
display.setTextWrap(false);
aggiornaDisplay(
false,
Meteo{},
millis()
);
} else {
Serial.println(
"OLED non disponibile."
);
}
if (!TEST_PIOGGIA) {
BaseType_t risultato = xTaskCreate(
taskMeteo,
"meteo",
8192,
nullptr,
1,
nullptr
);
if (risultato != pdPASS) {
Serial.println(
"Impossibile avviare il task meteo."
);
}
}
Serial.println(
TEST_PIOGGIA
? "MODALITA TEST: pioggia simulata."
: "Portaombrelli pronto."
);
}
// ---------- Loop ----------
void loop() {
uint32_t adesso = millis();
Meteo dati = leggiMeteo();
bool movimento =
digitalRead(PIN_PIR) == HIGH;
gestisciMovimento(
movimento,
dati,
adesso
);
aggiornaAllarme(adesso);
static uint32_t ultimoDisplayMs = 0;
if (
static_cast<uint32_t>(
adesso - ultimoDisplayMs
) >= 200
) {
aggiornaDisplay(
movimento,
dati,
adesso
);
ultimoDisplayMs = adesso;
}
delay(10);
}